Course outline · 0% complete

0/28 lessons0%

Course overview →

The stack and the heap

lesson 4-1 · ~12 min · 11/28

From lesson 3-1, two processes running the same program do not see each other's changes because each process gets its own private memory, isolated by the OS.

That is process isolation. The OS gives every process its own address space and the hardware enforces the boundary on every access.

This lesson looks inside one process's memory to see how that space is organized.

Inside one process's memory

This layout is worth learning because it explains, precisely, the most common beginner bug in Python, which is a list changing when a different variable was modified. It also explains what a stack trace is actually showing and why the crashes in the next lesson happen. It is a perennial interview topic on top of that.

The RAM a process owns is organized into regions, and two of them matter here:

RegionHoldsLifetime
stackfunction calls, one frame eachuntil the function returns
heaplists, dicts, objectsuntil nothing points at it

The stack holds function calls. Every time a function is called, a frame is pushed on top carrying its parameters, its local variables, and where to return to. When the function returns, its frame is popped off. Last in, first out, like a stack of plates.

The heap holds data whose lifetime is not tied to one function call.

In Python, local names live in stack frames, but the objects they point to, meaning lists, dicts, strings, and numbers, live on the heap. A variable is an arrow from a frame to a heap object, and that single sentence is the model the rest of this unit builds on.

STACK (function calls)frame: main()frame: totals() a →frame: helper() b →frame: helper() c →HEAP (objects)[1, 2, 3, 4]{"name": "Ada"}
Stack frames hold names. The objects live on the heap. Note two different names, a and b, pointing at the same gold list.

Two arrows, one object

The figure shows the trap this model predicts. b = a does not copy the list, it copies the arrow. Both names now point at the same heap object, so a change made through either name is visible through both.

This is the number one "my data changed on its own" bug for Python beginners, and the arrow model makes the reason visible rather than mysterious.

Python checks identity with the is operator, which asks whether two names point at the same heap object. That is a different question from ==, which asks whether two objects hold equal contents.

ExpressionQuestion
a is bsame heap object
a == bequal contents

Two separate lists holding [1, 2, 3] are == but not is, and that distinction is exactly what the next two examples measure.

Assignment copies the arrow

One list, two names, and an append through the second name.

a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(a is b)

Output

[1, 2, 3, 4]
True

a shows the appended element even though the append was written through b, because there is one list object on the heap and two names on the stack pointing at it.

a is b printing True is the confirmation. No copy was ever made, so any method that mutates the list in place is visible through every name that refers to it, which is why passing a list into a function can change the caller's data.

Making a genuine copy

list(c) builds a new heap object, so c is untouched by changes to d.

c = [1, 2, 3]
d = list(c)
d.append(4)
print(c)
print(d)
print(c is d)

Output

[1, 2, 3]
[1, 2, 3, 4]
False

list(c), c.copy(), and c[:] all build a new list object on the heap, and any of the three would work here. After a real copy, c is d prints False, which is the signature of two separate heap objects.

These are shallow copies, which matters as soon as the list holds other lists. The outer list is new, but the inner objects are still shared arrows, so copy.deepcopy exists for the case where every level must be independent.

Where the name lives versus where the object lives

For nums = [1, 2, 3] written inside a function, the name nums lives in the current stack frame and the list object lives on the heap.

Names live in the frame and vanish when the function returns. The list object survives as long as anything still points at it, which is how returning a list from a function works at all.

ThingRegionGone when
the name numsstack framethe function returns
the list objectheapno arrows remain

If objects lived in the frame, every return would destroy the data and no function could hand a result back. The split is what makes returning values possible, and it is also why an object kept alive by a forgotten reference is a memory leak rather than an automatic cleanup.