Skip to content

Stack-Heap Interaction

Name-Object Binding

1. Names on Stack

def function():
    x = [1, 2, 3]   # x on stack
                    # [1,2,3] on heap

Memory:

Stack:          Heap:
[frame]
  x -------->  [1, 2, 3]

2. Multiple Names

def function():
    x = [1, 2, 3]
    y = x
    z = x

Memory:

Stack:          Heap:
[frame]
  x -------->
  y -------->  [1, 2, 3]
  z -------->

Function Calls

1. Parameter Passing

def process(lst):
    lst.append(4)

data = [1, 2, 3]
process(data)

Memory:

Stack:              Heap:
[main]
  data -------->
                    [1, 2, 3]
[process]
  lst -------->

2. Return Values

def create():
    x = [1, 2, 3]
    return x

result = create()

After return:

Stack:          Heap:
[main]
  result ----> [1, 2, 3]

Scope Impact

1. Local Scope

def outer():
    x = [1, 2, 3]   # x in outer frame

    def inner():
        y = x       # y in inner frame
                    # Both point to heap
    inner()

2. Global Scope

GLOBAL = [1, 2, 3]  # Global frame

def function():
    local = GLOBAL   # Local frame
                     # Both point to heap

Object Lifetime

1. Outlives Frame

def create():
    x = [1, 2, 3]
    return x
    # x removed from stack
    # Object stays on heap

result = create()
# Object still accessible

2. Multiple References

def function():
    x = [1, 2, 3]
    global GLOBAL
    GLOBAL = x
    # x removed at return
    # Object kept by GLOBAL

Closures

1. Captured Variables

def outer():
    x = [1, 2, 3]   # Heap object

    def inner():
        return x    # Captures reference

    return inner

f = outer()
# outer frame gone
# x kept for closure

Memory Efficiency

1. Sharing Objects

# Efficient: one object
data = [1, 2, 3]
refs = [data] * 100

# 100 stack entries
# 1 heap object

2. Copying Objects

# Inefficient: many objects
refs = [
    [1, 2, 3].copy()
    for _ in range(100)
]

# 100 stack entries
# 100 heap objects

Summary

1. Interaction

  • Names on stack
  • Objects on heap
  • Names point to objects
  • Multiple names → one object

2. Lifetime

  • Stack: function scope
  • Heap: until GC'd
  • Objects outlive frames

Runnable Example: variables_and_memory.py

"""
01_beginner_variables_and_memory.py

TOPIC: Introduction to Variables and Memory in Python
LEVEL: Beginner
DURATION: 45-60 minutes

LEARNING OBJECTIVES:
1. Understand what variables are in Python (names that reference objects)
2. Learn the difference between stack and heap memory
3. Explore Python's object model
4. Use id() to inspect memory addresses
5. Understand the concept of "everything is an object"

KEY CONCEPTS:
- Variables are names, not containers
- Objects live in heap memory
- Variable names are stored in namespaces (on the stack)
- id() returns the memory address of an object
- Python uses "pass by object reference" semantics
"""

# ============================================================================
# SECTION 1: Understanding Variables as References
# ============================================================================

if __name__ == "__main__":

    print("=" * 70)
    print("SECTION 1: Variables as References to Objects")
    print("=" * 70)

    # In Python, variables are NOT boxes that contain values.
    # Instead, variables are NAMES that REFERENCE objects in memory.

    # When we write this:
    x = 42

    # What actually happens:
    # 1. Python creates an integer object with value 42 in HEAP memory
    # 2. Python creates a name 'x' in the current namespace (STACK memory)
    # 3. Python makes 'x' REFERENCE (point to) the integer object

    # We can see WHERE in memory the object lives using id():
    print(f"\nVariable x references value: {x}")
    print(f"Memory address (id) of object: {id(x)}")
    print(f"Type of object: {type(x)}")

    # The id() function returns a unique identifier for an object
    # In CPython (the standard Python implementation), this is the memory address

    # ============================================================================
    # SECTION 2: Multiple Variables Can Reference the Same Object
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 2: Multiple Names for the Same Object")
    print("=" * 70)

    # Let's create another variable that references the same object
    y = x

    print(f"\nx = {x}, id(x) = {id(x)}")
    print(f"y = {y}, id(y) = {id(y)}")

    # Notice: id(x) and id(y) are THE SAME!
    # This means x and y are TWO NAMES for the SAME object in memory
    print(f"\nAre x and y the same object? {id(x) == id(y)}")

    # We can also use the 'is' operator to test object identity
    print(f"Using 'is' operator: x is y = {x is y}")

    # MEMORY MODEL:
    #
    # STACK (Namespace)          HEAP (Objects)
    # ┌──────────────┐           ┌──────────────┐
    # │ x  ───────────┼──────────>│   int: 42    │
    # │ y  ───────────┼──────────>│              │
    # └──────────────┘           └──────────────┘
    #
    # Both x and y point to the same integer object in heap memory

    # ============================================================================
    # SECTION 3: Assignment Creates New References, Not New Objects (Sometimes)
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 3: Understanding Assignment")
    print("=" * 70)

    # For small integers, Python uses a technique called "integer interning"
    # This means Python REUSES objects for common integers (-5 to 256)

    a = 100
    b = 100

    print(f"\na = {a}, id(a) = {id(a)}")
    print(f"b = {b}, id(b) = {id(b)}")
    print(f"Are a and b the same object? {a is b}")

    # For larger integers, Python creates separate objects
    # (Note: This behavior can vary based on how integers are created)

    large_a = 1000
    large_b = 1000

    print(f"\nlarge_a = {large_a}, id(large_a) = {id(large_a)}")
    print(f"large_b = {large_b}, id(large_b) = {id(large_b)}")
    print(f"Are large_a and large_b the same object? {large_a is large_b}")

    # IMPORTANT: This shows that Python optimizes memory for common values!

    # ============================================================================
    # SECTION 4: Reassignment Changes What a Variable References
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 4: Reassignment")
    print("=" * 70)

    # Let's start with a variable
    num = 10
    print(f"Initially: num = {num}, id = {id(num)}")

    # Now reassign it
    old_id = id(num)
    num = 20
    new_id = id(num)

    print(f"After reassignment: num = {num}, id = {id(num)}")
    print(f"Did the id change? {old_id != new_id}")

    # WHAT HAPPENED:
    # 1. num originally referenced an int object with value 10
    # 2. Assignment num = 20 makes num reference a DIFFERENT int object (value 20)
    # 3. The original object (10) might be garbage collected if nothing else references it
    #
    # MEMORY MODEL BEFORE:          MEMORY MODEL AFTER:
    # STACK      HEAP               STACK      HEAP
    # ┌────┐    ┌────────┐         ┌────┐    ┌────────┐
    # │num─┼───>│ int:10 │         │num─┼───>│ int:20 │
    # └────┘    └────────┘         └────┘    └────────┘
    #                                         ┌────────┐
    #                                         │ int:10 │ (may be garbage collected)
    #                                         └────────┘

    # ============================================================================
    # SECTION 5: Stack vs Heap Memory in Python
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 5: Stack vs Heap Memory")
    print("=" * 70)

    # STACK MEMORY:
    # - Stores function call information (call stack)
    # - Stores local variable NAMES (references)
    # - Stores function parameters
    # - Fixed size, automatically managed
    # - Very fast access
    # - Small in size

    # HEAP MEMORY:
    # - Stores all OBJECTS (integers, strings, lists, custom objects, etc.)
    # - Dynamically allocated
    # - Managed by Python's memory manager and garbage collector
    # - Slower than stack but more flexible
    # - Much larger than stack

    def demonstrate_stack_heap():
        """
        Function to demonstrate stack and heap usage
        """
        # When this function is called:
        # 1. A new frame is pushed onto the CALL STACK
        # 2. Local variable names are stored in this frame's namespace (STACK)
        # 3. Objects are created in HEAP memory

        local_var = "Hello"  # 'local_var' name on stack, string object in heap

        print(f"\nInside function:")
        print(f"  local_var = {local_var}")
        print(f"  id(local_var) = {id(local_var)}")

        return local_var  # Returns the reference, not the object itself

    result = demonstrate_stack_heap()
    print(f"\nOutside function:")
    print(f"  result = {result}")
    print(f"  id(result) = {id(result)}")

    # Notice: The id is the same! The string object is in heap memory,
    # and both local_var and result are just names that reference it.

    # When demonstrate_stack_heap() finishes:
    # - The function's stack frame is removed
    # - local_var name no longer exists
    # - But the string object STAYS in heap (because 'result' references it)

    # ============================================================================
    # SECTION 6: Everything is an Object in Python
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 6: Everything is an Object")
    print("=" * 70)

    # In Python, EVERYTHING is an object, including:
    # - Numbers
    # - Strings
    # - Functions
    # - Classes
    # - Modules
    # - Even types themselves!

    # Let's verify this:
    items = [
        42,                    # Integer
        3.14,                  # Float
        "Hello",              # String
        [1, 2, 3],           # List
        demonstrate_stack_heap, # Function
        int,                  # Type
    ]

    print("\nDemonstrating that everything has an id (memory address):")
    for item in items:
        print(f"  {str(item):30} -> id: {id(item)}, type: {type(item).__name__}")

    # ============================================================================
    # SECTION 7: The Difference Between '==' and 'is'
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 7: Equality vs Identity")
    print("=" * 70)

    # == compares VALUES (calls the __eq__ method)
    # is compares IDENTITY (checks if same object in memory)

    list1 = [1, 2, 3]
    list2 = [1, 2, 3]
    list3 = list1

    print(f"\nlist1 = {list1}, id = {id(list1)}")
    print(f"list2 = {list2}, id = {id(list2)}")
    print(f"list3 = {list3}, id = {id(list3)}")

    print(f"\nlist1 == list2: {list1 == list2}  # Same values")
    print(f"list1 is list2: {list1 is list2}  # Different objects")

    print(f"\nlist1 == list3: {list1 == list3}  # Same values")
    print(f"list1 is list3: {list1 is list3}  # Same object")

    # MEMORY MODEL:
    # STACK           HEAP
    # ┌──────┐       ┌──────────────┐
    # │list1─┼──────>│ [1, 2, 3]    │
    # │list3─┼──────>│              │
    # └──────┘       └──────────────┘
    # ┌──────┐       ┌──────────────┐
    # │list2─┼──────>│ [1, 2, 3]    │
    # └──────┘       └──────────────┘

    # ============================================================================
    # SECTION 8: Understanding sys.getsizeof()
    # ============================================================================

    print("\n" + "=" * 70)
    print("SECTION 8: Object Size in Memory")
    print("=" * 70)

    import sys

    # sys.getsizeof() tells us how many bytes an object occupies in memory

    objects = [
        42,
        "Hello",
        [1, 2, 3],
        {"key": "value"},
    ]

    print("\nMemory sizes of different objects:")
    for obj in objects:
        print(f"  {str(obj):30} -> {sys.getsizeof(obj)} bytes")

    # Notice:
    # - Even simple integers take up memory (28 bytes on most systems)
    # - This is because Python stores additional information:
    #   * Reference count
    #   * Type information
    #   * Object header

    # ============================================================================
    # SECTION 9: Key Takeaways
    # ============================================================================

    print("\n" + "=" * 70)
    print("KEY TAKEAWAYS")
    print("=" * 70)

    print("""
    1. Variables are NAMES that REFERENCE objects, not containers
    2. Objects live in HEAP memory
    3. Variable names are stored in STACK memory (in namespaces)
    4. Multiple variables can reference the same object
    5. id() gives us the memory address (identity) of an object
    6. 'is' checks object identity, '==' checks value equality
    7. Everything in Python is an object (has id, type, and value)
    8. Assignment creates new references, not necessarily new objects
    9. Python optimizes memory by reusing objects for common values
    10. Understanding memory is crucial for writing efficient code
    """)

    # ============================================================================
    # EXERCISES TO TRY:
    # ============================================================================

    print("\n" + "=" * 70)
    print("PRACTICE EXERCISES")
    print("=" * 70)

    print("""
    Try these exercises to test your understanding:

    1. Create two variables with the same string value and check if they're
       the same object using 'is'. Try with short strings and long strings.

    2. Create a list and assign it to two different variables. Modify the list
       through one variable and observe what happens to the other.

    3. Use id() to track what happens to an object when you reassign a variable.

    4. Write a function that takes a parameter and prints its id before and
       after reassigning the parameter inside the function.

    5. Compare the memory size (using sys.getsizeof()) of an empty list,
       a list with 10 elements, and a list with 100 elements.

    See exercises_01_beginner.py for complete practice problems!
    """)