Frames
Line 5 calls make_box(3, 4). Python pushes a new frame onto the call stack: scratch space for this one call. It holds the parameters w and h, room for local variables, and where to go back to: line 5.
Objects on the heap
The numbers are not inside the frame. In Python every value is an object on the heap, a large shared area of memory. The names w and h only hold references: arrows to the objects 3 and 4.
Line 2 builds a list. Python takes room for a new list object on the heap, fills it, and binds the local name box to it. The list holds references too: to the same 3 and 4.
return box hands back a reference, not a copy. The frame is popped and w, h and box are gone. The list lives on, because b in the main frame now refers to it. Objects can outlive the call that made them.
The stack is tidy: frames are pushed and popped in last-in, first-out order as calls start and end. The heap is for objects that may outlive any one call, so they stay as long as something refers to them.
Freeing and limits
del b removes the name b. Now nothing refers to the list, so CPython frees it at once and its room on the heap can be reused. CPython counts references to every object; you never free memory yourself.
Every running call holds a frame, and the stack is limited. A function that keeps calling itself with no base case pushes frames until Python stops it with a RecursionError, by default at 1000 deep. That is a stack overflow.
C by hand
C does not manage the heap for you. malloc(8) asks for 8 bytes on the heap and returns their address. The pointer p keeps that address, and p itself is a plain local variable in the call’s stack frame.
When done, you must call free(p) yourself. Forget it and the memory leaks, unused until the program ends. Use p after free and the behaviour is undefined, a classic bug. Python frees objects for you instead.
Try it
;; step 1/9 · line 5: b = make_box(3, 4) >>> def make_box(w, h): ... box = [w, h] ... return box ... >>> b = make_box(3, 4) >>> b [3, 4]
The drawing shows one block per running call. In CPython each call gets a frame; frame.f_back links it to the caller’s frame, which remembers where it paused. In machine code, as compiled from C, the call itself pushes a return address on the stack. CPython makes small ints, from -5 to 256, once and shares them, so the 3 and 4 already exist before the call.