python/volume-1/chapter-09ch 09 / 64 · 22 min

09Machines with names

In Chapter 8 we learned to pick the right container on sight. In this one we build the machines that work on them. I want to go slow here, because most tutorials walk straight past one question. Here's the plan. We bundle a run of steps into a named unit and call it again and again. We hand it 'Blinding Lights' at 200s, then 'Titanium' at 245s — the same machine both times. Then we steer exactly how those arguments find their parameters, and tell whether a call hands back a value or a quiet None. The whole way through, we keep asking the one thing that actually matters: what does def really build in memory, and what wakes it the moment a pair of parentheses finally runs? By the end we've watched frames push and pop on the call stack, climbed the LEGB ladder Python uses to resolve a name, and met the strange part: a function is itself a value, so you can pass the machine, not just its output.

A machine with a name How arguments find their parameters What comes back: return, or None The call stack: frames that push and pop
scroll to begin

01A machine with a name

Let's start with the pain, because it's the whole reason functions exist. Printing one song takes three lines. Printing forty songs, three lines each, would be a hundred and twenty lines: the same three lines copied out thirty-nine needless times, and every copy is a fresh chance to fumble one. Now watch the better move. Write the three lines once, hang a name on the bundle, and from then on just say the name. That's the whole of def: bundle the steps, name the bundle. You're building a small machine and writing its name on the side.

describe.pypython
def describe(title, artist, seconds):
    line = title + " by " + artist
    print(line, "-", seconds, "s")

describe("Blinding Lights", "The Weeknd", 200)   # Blinding Lights by The Weeknd - 200 s

Look hard at the two moments in that snippet, because they are genuinely different things. The names in the def line — title, artist, seconds — are the parameters: labelled empty slots built into the machine's design, waiting to be filled. The values you drop in at the call — "Blinding Lights", "The Weeknd", 200 — are the arguments: what actually arrives when you press GO. Here's a mnemonic worth keeping. The parameter is part of the machine; the argument is what shows up at the door.

In memory, def does something you have already met. It builds a function object on the heap — the machine itself, holding the compiled body plus the list of parameter names. Then it binds the name describe to that object, exactly the way a = 10 bound a name to an int back in chapter 3. The object is compact, about 160 bytes on this machine, and it mostly just points at the separate 240-byte block of compiled instructions that is the body. Then comes the part that surprises everyone: the body does not run. Not one line of it. def executes, the machine gets built and named, and the instructions inside sit shelved and untouched, waiting for a caller.

THE ONE IDEA TO CARRY FORWARD
A function is an object, and def is two tiny acts glued together: build the machine on the heap, then bind a name to it. Everything else in this chapter — arguments, returns, scope, closures — is only what happens after a pair of parentheses finally wakes that shelved body.
Parameters are the slots; arguments are what you drop infigure
the parameter is part of the machine’s design · the argument is what arrives when you press GO describe the machine parameters · built-in empty slots title artist seconds arguments · values at the call "Blinding Lights" "The Weeknd" 200
Fig 01 — The three parameters are empty slots stamped into describe's design the moment def runs. The three arguments only exist at the call, arriving to fill those slots. Design on the left, delivery on the right.
Wait — if the body never runs at def, what does the bare word describe, with no parentheses, actually give me? The machine itself. Python hands you the object, which prints as <function describe at 0x…> — a live thing you can rename or pass around. Only the parentheses call it. So describe is the noun; describe(…) is the verb.
def is a statement, not a rehearsal
Python really does execute the def line the instant it reaches it — but executing it only builds the object and binds the name. The body is compiled and set on a shelf; it stays there, never yet run, until parentheses press GO. Reaching a def is not the same as running what is inside it.
Step through the lifecycle — watch build, then bind, then GOinteractive
① reach the def line — nothing built yet source.py def describe(title, artist, seconds): print(line, "-", seconds, "s") describe("Blinding Lights", …, 200) function object · heap params: title, artist, seconds body: compiled — not run yet describe body run?  NO console (nothing printed yet)
① reach def
Drag from ① to ③. At ② the machine appears and the name binds — yet the console stays empty, because the body is still asleep. Only at ③, when the parentheses press GO, does a single line finally print.
Fig 02 — One def, two quiet acts (build, bind), and a body that waits. The call is a separate event — the only one that ever runs your code.
↺ reframe
Stop reading def describe(…) as "here is where the printing happens." Read it as describe = <a machine> — an assignment wearing a costume. The name and the machine are two separate things: one is a label on a shelf, the other is the object sitting on it. Once that split clicks, everything a function can do next follows from it.
first_class.pypython
encore = describe            # a second name for the SAME machine
encore is describe           # True  -> one object, two names
encore("Titanium", "David Guetta ft. Sia", 245)
# Titanium by David Guetta ft. Sia - 245 s

del describe               # drop the original name...
encore("Blinding Lights", "The Weeknd", 200)
# ...still works: the machine outlived the label

Because the machine is just an object, a name is nothing precious to it. You can hang a second name on the very same machine, and both point at one object (encore is describe is True, same id). Delete the original name and the machine lives on, still answering to encore. Even its __name__ stays "describe", because that was baked in at build time, not carried by the label. This is what people mean when they call functions first-class: a function is a value like any other, free to be renamed, stored in a list, or handed to another function. We pull hard on that thread later in the chapter.

✗ The myth

def runs the code you wrote inside it. Defining a function and executing its body are the same event.

✓ The reality

def only builds the machine and hangs a name on it. The body sleeps — compiled and shelved — until a pair of parentheses wakes it. A function riddled with runtime bugs defines without a whimper; it breaks only the day you finally call it.
The deeper cut — in what sense is the body already "compiled"?

We keep saying def "compiles the body." That is a labelled shortcut, so here is the truer picture. The bytecode was actually made earlier. When Python first read the whole file, it compiled every function body into a code object and tucked it away as a constant, before a single statement ran. What the def statement does at run time is lighter: it grabs that ready-made code object, wraps it in a fresh function object (stapling on the parameter defaults, the surrounding variables it can see, and the name describe), drops that object on the heap, and binds the name. So "def compiles the body" really means "def wraps an already-compiled body."

You can feel the seam. A typo like x = = 5 inside a function you never call still refuses to run the file at all. It dies at compile time, before any def executes, because syntax is checked once, up front. But a reference to a name that does not exist sails straight through def untouched, and only raises the instant you call, because meaning is checked only when the body finally runs. Syntax up front, meaning on demand. That distinction pays off the day you meet closures and decorators.

You have been feeding three arguments in strict left-to-right order, and the slots filled up quietly. But nothing yet forces the call to match the design — so how does each argument find its parameter when the counts line up, and what happens the moment they don't? →

Wait — a function is an object, which means you can pin sticky notes to it. Write describe.genre = "synthpop" and it just works — Python lets you bolt arbitrary attributes straight onto a function, because under the costume it is an ordinary object with room to spare. The machine can carry notes about itself.
THE BODY EXISTS BEFORE def RUNS
def does not compile the body — Python did that the instant it first read the file, tucking the compiled instructions away as a ready-made code object among the module's constants. What the def line does at run time is lighter: it emits one MAKE_FUNCTION instruction that wraps that constant in a fresh function object and binds the name. The blueprint was already on the shelf; def just stamps a serial number on it.
Wait — the whole describe machine is just 160 bytes — and every extra name you hang on it is only an 8-byte arrow, not a copy. encore = describe makes encore is describe return True; a thousand aliases would still point at the one object. Delete the original name and the machine lives on, still remembering it was born "describe".
SYNTAX UP FRONT, MEANING ON DEMAND
Because def never looks inside the body, a function riddled with runtime bugs defines in perfect silence — it breaks only the day you finally call it. But a typo is caught earlier and harder: a single x = = 5 inside a function you never call still refuses to run the entire file, because syntax is checked once, up front, before any def executes.
Trace3 step the machine — idea · code · memory move together
def in slow motion — build the machine, bind the name, then GO
A def statement is two quiet acts: build a function object on the heap, then bind a name to it. The compiled body stays shelved and unrun until a pair of parentheses finally presses GO. Press Next and watch build, bind, alias and call move as one.
build + bindfunction · 160 Bbody runs only on ( )
Three zones — names · heap · rundef · lifecyclecompile
compile · body already → <code object describe>, shelved as a module constant
Name tablemodule
describe
encore
no names bound yet
Heapobjects
function 160 B
params: title, artist, seconds
code: → shelved bytecode
body: shelved
heap empty
Runbody?
body run?NO
console · stdout
(nothing printed yet)
In plain words
Under the hood
CONSTALU · the operation
Program · describe.py
in scope · type
Memory · heap object + names
registers — the running state
heap · function object the machine that got built
names → object
encore is describe → True
what's happening
beat 1 / 1

02How arguments find their parameters

You already met the two sides of a call. The parameters are the named slots you wrote in the def line — empty boxes waiting to be filled. The arguments are the actual values you hand over at the call site. The moment you write add_song(...), Python has to decide which value drops into which slot. It does this by a small, fixed rulebook — not by guessing, not by type, but by three plain mechanisms you can learn in one sitting.

Positional is the default: values fill slots left to right, first argument into the first slot. Keyword lets a value name its own destination — seconds=200 flies straight to the seconds slot no matter where you write it, whatever the order. A default is a fallback value baked into the def line, used only for a slot the call leaves empty. One traffic rule binds them: positionals first, keywords after. Once you switch to naming, you can't switch back. A bare value sitting behind a named one is a hard error, caught before your code even runs.

calls.pypython
def add_song(title, artist="Unknown", seconds=180):
    return f"{title} by {artist} - {seconds} s"

add_song("Blinding Lights", "The Weeknd", 200)      # by position
add_song("Blinding Lights", seconds=200, artist="The Weeknd")  # by name, reordered
add_song("Titanium")     # Titanium by Unknown - 180 s  (both defaults kick in)
Positionals fill in order · keywords fly to their namefigure
add_song("Blinding Lights", seconds=200, artist="The Weeknd") "Blinding Lights" seconds=200 artist="The Weeknd" slot 1, by order matched by name → title "Blinding Lights" artist "The Weeknd" seconds 200 the blue boxes are the parameter slots — a default would fill any the call left empty
Fig 06 — Positionals fill slots left-to-right; keywords ignore order and fly straight to the slot carrying their name. Same call, same result either way.

That is the whole rulebook, but rules are easier to trust once you've watched them run. Drag through five real calls below. The same three slots stay put; only what lands in them changes, colour-coded by how it got there.

Drive the call — watch each argument route to a slotinteractive
the call: add_song("Blinding Lights", "The Weeknd", 200) title "Blinding Lights" ← positional artist "The Weeknd" ← positional seconds 200 ← positional returns: "Blinding Lights by The Weeknd - 200 s" Three values, three slots, filled strictly by position — the plainest call there is. positional keyword default (from the def line)
1 / 5
Slide through: all-positional, the same call reordered by keyword, both defaults kicking in, the classic slip where a bare 245 lands in artist instead of seconds, and finally the one arrangement Python refuses to even compile.
Fig 06b — The slots never move; the binding does. Green means filled by position, gold by name, blue by a default — and the last form never runs at all.
THE SLIP THE FIGURE JUST SHOWED YOU
add_song("Titanium", 245) raises no error, yet it's almost certainly a bug: 245 filled artist because that's the second slot, and seconds quietly kept its default of 180. This is exactly why long calls read better with keywords — add_song("Titanium", seconds=245) can't miss. Position is terse; names are safe.

So far every value has been a lone thing dropped into a named slot. But a function can also open its arms and accept however many values you throw. Two star-markers in the def line turn the overflow into a single collected object. *args scoops up every spare positional argument into a tuple, and **kwargs scoops up every spare keyword argument into a dict. These are the very structures you built by hand in chapters 6 and 7, packed for you now at the instant of the call. (The names args and kwargs are only convention; the stars do the actual work.)

collectors.pypython
def playlist_length(*songs):        # songs is a TUPLE Python packs for you
    total = 0
    for s in songs:                 # walk the tuple it just built
        total += s
    return total                    # 799 — full story on return next section

playlist_length(200, 245, 354)      # songs == (200, 245, 354)

def tag_song(title, **extras):      # extras is a DICT
    for key, value in extras.items():
        print(title, key, value)

tag_song("Blinding Lights", artist="The Weeknd", year=2019)
Overflow gets collected — extras into a tuple, named extras into a dictfigure
playlist_length(200, 245, 354) 200 245 354 songs = (200, 245, 354) *args → a tuple (chapter 6) tag_song("Blinding Lights", …) artist="The Weeknd" year=2019 extras = {"artist": "The Weeknd", "year": 2019} **kwargs → a dict (chapter 7)
Fig 06c — Spare positionals land in a tuple, spare keywords in a dict. Nothing is dropped, and you never build the container yourself — the stars do.
Wait — if a star in the def line gathers loose values into a tuple, what does a star at the call site do? The mirror image: it scatters a tuple or dict back into separate arguments. Given row = ("Blinding Lights", "The Weeknd", 200), the call add_song(*row) spreads those three across title, artist, seconds exactly as if you'd typed them out; and add_song("Blinding Lights", **{"seconds": 200}) unpacks a dict into keyword arguments. Same symbol, opposite directions — one gathers, one spreads — depending on which side of the call it stands.

One question is easy to get wrong, and getting it wrong causes real bugs. When a value slides into a slot, is the function handed a copy, or the original itself?

✗ The myth

Arguments are copied into the function ("pass by value"), so whatever happens inside is sealed off — nothing a function does to its parameters can ever leak back out.

✓ The reality

No copy is made. The parameter becomes a second name for the very same object — chapter 3's aliasing, happening fresh at every call. Rebinding the name (s = s + 1) only points that one local name elsewhere and is invisible outside. Mutating the shared object (songs.append(...)) is fully visible, because caller and function are holding the same thing. Python calls this pass-by-object-reference.
↺ reframe
Don't picture arguments travelling into the function. Picture the function's parameter names as sticky labels, and at call time each label is slapped onto an object the caller already owns. title isn't a box holding "Blinding Lights" — it's a second tag hanging off the one string object. That's why passing a list lets a function reach back and change the caller's list, but passing a number never surprises anyone: numbers are immutable, so the only thing you can do is re-label.
THE MUTABLE-DEFAULT TRAP
def add(song, playlist=[]) builds one list, once, at def time — and every call that omits playlist reuses that same list. So add("Blinding Lights") then add("Titanium") returns ['Blinding Lights', 'Titanium'], not the fresh ['Titanium'] you expected. The fix is a fixed idiom: default to None, then build the real list inside the body.
safe_default.pypython
def add(song, playlist=None):
    if playlist is None:      # a brand-new list per call that needs one
        playlist = []
    playlist.append(song)
    return playlist
The deeper cut — the full signature order, and why that default is shared

The complete ordering of a parameter list is fixed: plain parameters, then *args, then keyword-only parameters, then **kwargs. Two bare markers let you draw harder lines. Names before a lone / are positional-only, so callers may not name them. Names after a lone * are keyword-only, so callers must name them:

strict.pypython
def add_song(title, /, artist, *, seconds=180):
    return (title, artist, seconds)

add_song("Blinding Lights", "The Weeknd", seconds=200)   # ok
add_song(title="Blinding Lights", artist="The Weeknd")     # TypeError: title is positional-only
add_song("Blinding Lights", "The Weeknd", 200)          # TypeError: seconds is keyword-only

And here is the mechanism the trap above rides on. Default values are evaluated once, at the moment def runs — not rebuilt on each call — and stored on the function object itself, in add_song.__defaults__. For an immutable default like 180 you'd never notice. For a mutable one like [], that single stored list is the shared state that leaks between calls. A default isn't a recipe re-run each time. It's one object, made when the def line executes, and reused forever after.

So arguments flow in and settle into their slots. What flows back out when the work is done — and what does Python quietly hand you when a function never says a word? →

Wait — no copy is ever made when an argument slides into a slot. Print id(x) inside the function and it is the exact same number as the caller's object — the parameter is a second name for the very thing you passed, not a duplicate. That is why handing a function a list lets it reach back and change yours, while handing it a number never surprises anyone.
THE DEFAULT IS ONE OBJECT — AND YOU CAN WATCH IT
A default is built once, when def runs, and stored on the function itself in __defaults__. So def add(song, playlist=[])'s list is the same list every call — provable from outside: run add("Blinding Lights"), then read add.__defaults__ and watch it already holding (['Blinding Lights'],) before the next call even happens.
Wait — since defaults live in a plain tuple on the function object, you can rewrite them at runtime. Do add_song.__defaults__ = ("The Weeknd", 200) and from then on add_song("Blinding Lights") returns "Blinding Lights by The Weeknd - 200 s" — you changed what "empty" means for every future call, without touching the def line at all.
Trace3 step the machine — idea · code · memory move together
Routing the call — positional by order, keyword by name, default for the rest
Python fills a function's parameter slots by a small fixed rulebook: positionals drop in left‑to‑right, keywords fly straight to the slot bearing their name, and any slot left empty falls back to its def‑line default. This walk traces the clean success; the two ways it goes wrong — a bare value silently landing in the wrong slot, and a positional after a keyword that won't even compile — are the last two rungs of the slider just above. Press Next and watch three arguments find their slots.
3 slots1 positional2 keyword
The rulebook — position, then name, then defaultcall · add_songset up
Argument queue · call order → Parameter slots ↓ Blinding Lights seconds = 200 artist = "The Weeknd" title required · no default · empty artist default 'Unknown' · empty seconds default 180 · empty ↷ skipped — for now Defaults bench fires only for an empty slot artist = 'Unknown' seconds = 180
In plain words
Under the hood
READALU · the operation
Program · calls.py
slots · type
Memory · slots + queue
registers — the running state
frame · add_song three slots · source of each binding
what's happening
beat 1 / 1
Trace3 step the machine — idea · code · memory move together
Arguments slot by position — get the order wrong and it lies
Positional arguments fill parameters strictly left to right — by slot number, never by name or meaning. Hand play(245, "Titanium") to def play(title, seconds) and Python silently mis-slots them: title=245, seconds="Titanium", with no error at all — just a confident, wrong answer. Naming the arguments routes them correctly in any order; a bare value after a named one is refused before a line runs.
3 call forms1 silent lie1 SyntaxError
Two values, two slots — routed by position, then by namedef play(title, seconds)call
title
empty
seconds
empty
READY
In plain words
Under the hood
BINDALU · the binding rule
Program · play.py
slots · type
Memory · the call frame
registers — the two slots
frame play() bound slots + return
title
seconds
returns
what is happening
beat 1 / 1

03What comes back: return, or None

A function runs, and then it answers. return is how it answers, and it does two things in the same breath: it stops the call on the spot, and it hands exactly one object back to whoever asked. That second part is the key idea. The call expression doesn't merely trigger the work, it becomes the result. Write total = playlist_length(200, 245) and, the instant the call returns, total is simply another name for 445. The parentheses have done their job; what's left standing where the call used to be is one plain value.

Lean on that word exactly. Not "up to one," not "as many as you like" — one object crosses back, always. And because return also ends the call the moment it fires, any line written after it in the same path never runs: the function has already left. That's less a rule to memorise than a consequence of the first job. You can't run more code after you've handed back your answer and gone.

The two jobs of return, in one linefigure
def playlist_length(a, b): total = a + b return total print("done!") ✗ never runs ② return also STOPS the call — the line below is dead ① it hands back exactly one object 445 total = 445 the call expression IS 445 now
Fig 03a — One return, two effects: the value 445 travels back and the name total becomes it (job ①), while the call ends so nothing below the return ever executes (job ②).
EVERY FUNCTION RETURNS SOMETHING
There is no such thing as a function that returns nothing. A call is an expression — it always evaluates to an object, exactly the way 200 + 245 evaluates to 445 — so a call is always legal on the right-hand side of an =. The one escape hatch is a call that raises instead of returning: then control leaves through the exception and no value comes back at all. Short of that, every call hands you an object — the only real question is whether it's worth keeping.

So what does a function hand back when it doesn't say return at all? It still answers — with None, Python's built-in nothing-object. Not a crash, not a blank, not the absence of a value. It's a specific, real object whose entire job is to mean "nothing worth returning happened here." You can catch it in your hands and print it:

silence.pypython
def shout(title):
    print(title.upper())      # shows on screen — but hands nothing back

echoed = shout("Titanium")    # TITANIUM appears here…
print(echoed)                 # None — the silence has a name

surprise = print("hi")        # careful: print is NOT return
print(surprise)               # None — print SHOWS a value, it never hands one back

That last pair is the trap that snares nearly everyone once. print and return feel like cousins, but they live on opposite sides of a wall. print draws characters on your screen for a human to read, then quietly returns None. return hands an object back to the program so more code can use it. A function that prints its answer but forgets to return it looks perfect on screen, yet gives you None the moment you try to do anything with the result.

TEST FOR NONE WITH is, NOT ==
There is only one None in a running program — every "nothing" is the very same object, a singleton. So the idiomatic, and slightly faster, check is if x is None: — you're asking "is this the nothing-object?" (an identity test), not "does this happen to look equal to it?"

Watch all of this move. Slide through four functions below: one that returns a number, one with a bare return, one with no return at all, and one that hands back a pair. In every case, whatever crosses the bridge is one object, and the name at the call site simply becomes it.

What crosses the bridge — drag through four functionsinteractive
def playlist_length(a, b): return a + b ↑ evaluates to 445 returns ▸ 445 hands back the call site becomes: total = 445 One int object crosses the bridge — total is now just a name for 445. every call evaluates to exactly one object — silence means None
1 / 4
Slide through all four. The object on the bridge changes — a number, then None twice (a bare return, then no return line at all), then a tuple of two values — but it is always exactly one object.
Fig 03b — The name on the right never does the work; it just becomes whatever single object the function hands back. Give nothing, and that one object is None.
↺ reframe
Stop reading a call as a command and start reading it as a value with extra steps. playlist_length(200, 245) isn't an instruction that "does 445" — after it returns, it simply is 445, and you can drop it anywhere a plain 445 could go: add it, compare it, print it, pass it straight into another call. A function call is a machine for turning arguments into one object, and the object is the whole point.

Which raises the obvious objection: what if you genuinely need two things back — a title and its length? You don't get to break the one-object rule, but you can bend it. Write return title, seconds and the comma quietly packs both into a single tuple. Remember, it's the comma that builds the tuple, not the parentheses. So still exactly one object makes the crossing. At the other end, chapter 6's unpacking splits it back apart in one clean move:

two_back.pypython
def longest_song():
    return "Titanium", 245        # the comma packs ONE tuple

pair = longest_song()             # pair  → ('Titanium', 245)  — a single tuple
title, seconds = longest_song()   # unpacked back into two names
print(seconds)                    # 245

✗ The myth

A function without a return "returns nothing," so its result is unusable — a dead end you can't assign or pass on.

✓ The reality

It returns None, a genuine object. You can capture it, pass it, and print it — it's just rarely useful. "Returns nothing" is loose talk for "returns None," and knowing the difference is what stops the None-that-should've-been-a-number bug.

Wait — while shout was running, a local name title existed for a few microseconds and then vanished the instant return fired. Where, exactly, did that name live — and what swept it away so cleanly the moment the call ended?
The deeper cut — bare return, falling off the end, and why it's always one object

Three things all funnel to the same None, and it's worth seeing they're truly identical. A bare return, a function whose body just runs off the end, and an empty pass body all hand back the exact same object. You can prove they're the same one, not merely equal, because None is a singleton: bare() is nothing() is True. Underneath, CPython isn't improvising this. Disassemble a silent function on Python 3.12 and the last instruction is literally RETURN_CONST None — the compiler wrote the None in for you. There is no code path out of a function that doesn't return an object. "No return statement" just means "the compiler supplied the return."

And the two-value case is the clearest proof that "exactly one object" is no white lie. Disassemble return "Titanium", 245 and you don't see two returns or a special multi-value opcode. You see one RETURN_CONST (('Titanium', 245)), the whole tuple built once as a single constant and handed back in a single move. The comma didn't return two things; it made one thing out of two. As for cost, that None is very cheap: sys.getsizeof(None) is just 16 bytes, and since it's a singleton, every None in your whole program shares that one 16-byte object rather than making a new one each time.

The local title lived in a private scratch record that Python built for that one call and destroyed the instant it returned — a frame. Frames stack up as calls nest inside calls and vanish in strict last-opened-first-closed order. Next: the call stack — frames that push and pop →

Wait — ask Python for a brand-new None and it hands you the old one. type(None)() — actually calling the None class — gives back an object where … is None is True. There is exactly one None in the whole running program, and even trying to manufacture another just points you back at it.
Wait — run print(print("hi")) and you see two lines: hi, then None. The inner print draws hi on screen and hands back None; the outer print then dutifully shows that None. It is the cleanest one-liner proof that print shows a value while return hands one back — opposite sides of a wall.
EVERY None IS THE SAME 16 BYTES
Return None a billion times and Python allocates nothing new — sys.getsizeof(None) is just 16 bytes, and every "nothing" in your program shares that one object. The empty tuple gets the same treatment: () is () is True. Some objects are so common Python keeps a single copy and points everyone at it.
THE COMPILER WRITES YOUR RETURN FOR YOU
A function with no return line is not special-cased at run time — disassemble a silent function on Python 3.12 and its last instruction is literally RETURN_CONST None. There is no path out of a function that does not hand back an object; "no return statement" just means the compiler quietly supplied one.

04The call stack: frames that push and pop

Chapter 8 had you build a stack by hand — a pile of plates where you only ever touch the top one. Here is the secret: your interpreter has been running one of those the entire time, underneath every function you have ever called. It is called the call stack. Once you can see how it works, functions stop being magic boxes and become exactly what they are — a tidy, self-cleaning pile of paperwork.

Every time you call a function, Python tears off a fresh sheet of scratch paper for that one call: a frame. The frame holds this call's local names — its parameters, and any variable it assigns — plus a bookmark remembering which line to come back to. Calling a function pushes its frame onto the top of the stack. Hitting return pops that frame straight back off: its local names are wiped, and the value it returned flows down into the frame that was waiting below. Last sheet on is the first sheet off. It's the very same LIFO discipline as the plate-stack from chapter 8, because it is one, wired directly into the interpreter.

That one mechanism answers a question you may not have known to ask: if I call the same function twice, why don't the two calls trip over each other's variables? Because each call got its own sheet of paper. Two frames, two private sets of locals, sitting on the stack at the same time and unable to see across the gap. The n inside one call is simply not the n inside another.

THE ONE IDEA TO CARRY FORWARD
A running program is a stack of frames, one per call still in progress. Calls push, returns pop, and only the top frame is actually running — everything beneath it is paused, holding its place, waiting for a value to drop down from above.

Take the smallest honest example from our playlist. One function turns minutes-and-seconds into a plain count of seconds; another adds two songs together. "Blinding Lights" is 3 min 20 s, so song_seconds(3, 20) is 200. "Titanium" is 4 min 5 s, so song_seconds(4, 5) is 245. Together, that's 445. Watch the three shapes the stack passes through as that runs.

one call before, two calls deep, empty afterfigure
before the call module total = ? two calls deep module total = ? total_seconds a = 200 b = ? song_seconds ← runs m = 4 s = 5 after both return module total = 445 calls PUSH ↑ returns POP ↓
Fig 1 — The top frame (gold) is the one running; every frame beneath it is paused, holding a half-finished line, waiting for a value to flow down. Two calls deep on the left, back down to one on the right.

A snapshot is a still photograph, though, and a stack is a motion picture. Drag the step below through the whole run — eight beats from the first line to the last. Watch each call push a frame, each return pop one and hand its answer down, and the highlighted line show exactly which sheet of paper is doing the work.

scrub the run — watch frames push and popinteractive
THE CALL STACK — grows upward ↑ frames: 1 def song_seconds(m, s): return m*60 + s def total_seconds(): a = song_seconds(3, 20) b = song_seconds(4, 5) return a + b total = total_seconds()
step 0 / 7
Calls push, returns pop — the last frame on is the first frame off (LIFO). The gold frame is the one running; the cyan values are its private locals, gone the instant it pops.
Fig 2 — The same run, one beat at a time. Notice that song_seconds is pushed, run, and destroyed twice — the second call gets a brand-new frame, never the recycled first one.
↻ reframe
You never "jump into" a function and leave the caller behind. The caller is right there underneath, frozen mid-line, its half-finished expression holding a slot for the value about to arrive. A program isn't a single point of execution racing around your file — it's a tower of paused work, and only the brick on top is moving.

One precise thing about what a frame actually stores. It does not hold the objects 200 and 245 themselves — it holds the names that point at them. The objects live out on the heap, the open warehouse we met in chapter 0, and a frame's locals are just labelled arrows aimed at that warehouse. So when a frame pops, the arrows vanish, but an object only disappears once nothing anywhere still points at it. Chapter 11 draws that border between stack and heap in full. For now, keep the two jobs apart: the stack tracks who is calling whom, and the heap holds the things themselves.

Wait — if every call pushes a fresh frame, what happens when a function calls itself? Nothing special: the stack just keeps growing, frame on frame on frame, each with its own private copy of the locals. That is recursion, and the call stack is exactly what makes it work — every level gets a clean sheet, so the levels never overwrite each other. Slide it and watch the tower rise.
a function calling itself — the tower risesinteractive
STACK — one frame per call ceiling ≈ 1000 → RecursionError def fact(n): if n == 1: return 1 return n * fact(n - 1) Each call pauses on the last line, waiting for fact(n-1) below it to hand back a value.
depth 4
The green frame at the top is the base casefact(1) returns without calling again, and the tower unwinds, each level multiplying as its child returns. Keep pushing and Python eventually says stop.
Fig 3 — Recursion is not a new mechanism — it is the ordinary push-and-pop, aimed at the same function. The stack's private-frame rule is the whole reason the levels don't clobber one another.

"Eventually says stop" is not a figure of speech. Each frame costs real memory, and a runaway recursion — a function that forgets its base case and calls itself forever — would eat the machine alive. So Python draws a hard line: ask sys.getrecursionlimit() and it answers 1000. Let a function call itself with no exit and the tower climbs fast, then slams into that ceiling just shy of the thousandth frame. At that point Python refuses to push another frame and raises RecursionError. It is not your program crashing randomly. It is the interpreter's seat belt, catching an infinite climb before it takes the whole process down.

DON'T JUST RAISE THE CEILING
You can nudge the limit with sys.setrecursionlimit(), but reaching for it is usually a sign the recursion has no working base case. The ceiling is deliberately set below the level where the real C stack underneath would overflow — and that overflow is a hard crash Python can't catch. A RecursionError is a gift: it turns a fatal crash into a catchable exception. Fix the base case, don't lift the ceiling.

Here is the part that pays for the whole section. You have watched the call stack print itself hundreds of times already — every time your code blew up. That wall of "File ..., line ..., in ..." is not noise. It is a photograph of the live stack at the instant something broke, frame by frame, oldest call first. The traceback is the call stack, made visible. (The raise ValueError("no such song") in the next block is how you throw an error on purpose. Here it's just a reliable way to break something so the stack prints; you'll build exceptions and raise properly in a later chapter.)

boom.pypython
def a_call():  return b_call()
def b_call():  return c_call()
def c_call(): raise ValueError("no such song")

a_call()
# Traceback (most recent call last):
#   File "boom.py", line 5, in <module>   a_call()      ← bottom of the stack
#   File "boom.py", line 1, in a_call     return b_call()
#   File "boom.py", line 2, in b_call     return c_call()
#   File "boom.py", line 3, in c_call     raise ValueError(...)   ← top, where it broke
# ValueError: no such song
↻ reframe — the traceback is a map, not a scolding
Read it like the stack it is. The first frame listed is the floor — <module>, where your program began. Each line below is one frame deeper, one more call still waiting for the one above it. The last frame is the top of the stack: the exact call that was running when it failed. "Most recent call last" is Python telling you plainly — this is the tower, printed floor to ceiling.

✗ The myth

Local variables are stored "inside the function," so two calls to the same function share them, and a value can leak from one call into the next.

✓ The reality

Locals live in the frame, and every call gets its own frame. The function is just the recipe; the frame is the kitchen. Ten simultaneous calls means ten kitchens — ten private copies of every local. The recursion above proves it, with a different n alive at every level.
The deeper cut — is "the stack" a real place in memory?

Saying frames sit on a separate "stack" of memory is a labelled simplification — true to the roles, looser about the geography. In CPython (the standard interpreter), each Python frame is itself a small heap object of type frame. You can even grab the running one with sys._getframe() and follow its f_back link down the whole tower, reading each frame's f_locals as you go. So "the stack" names the strict last-in-first-out discipline those frame objects obey — push on call, pop on return — not a fenced-off slab of hardware across town. The behaviour in every figure here is exactly right; only the literal "separate district" is the convenient lie, and now you know where it bends.

Why can this discipline be so cheap? Because it only ever changes at one end. Pushing a frame is barely more than advancing a single pointer, and popping it is moving that pointer back. There's no searching and no cleanup crew. That's why local names are fast and self-erasing, and why the real C-level stack has a fixed size that Python's 1000-frame ceiling is carefully set to stay under.

Every frame carries its own private names — so when the same word means one thing inside a function and another thing outside it, which one does Python actually reach for? Next: LEGB, the four-ring search Python runs to look up a name. →

PYTHON TURNS DOWN FREE SPEED
Many languages fold a tail call so a function that ends by calling another reuses one frame instead of stacking a new one. Python deliberately refuses: even() calling odd() calling even() twenty-thousand deep really stacks twenty-thousand frames and hits RecursionError. Every frame is kept on purpose — so the traceback stays an honest, complete map of how you got here.
Wait — the recursion limit is 1000, but count the frames and Python actually stops you at 999 — one sheet shy of the number. The ceiling sits just below the real C-stack cliff underneath, and that overflow is a hard crash Python cannot catch. So RecursionError is not your program failing; it is the seat belt tightening a frame early.
Wait — the 1000-frame ceiling is about frames, not arithmetic. A recursive factorial gives up near fact(1000) — but the same maths written as a loop sails straight past: math.factorial(2000) is a 5,736-digit number, computed instantly, so big Python will not even print it without lifting its own 4,300-digit safety cap. The stack ran out of paper; the numbers never blinked.
Wait — a recursion's frame tower can literally build a number. Define to_binary(n) that returns "" at 0 and otherwise to_binary(n // 2) + str(n % 2). Call to_binary(6) and it stacks a frame for 6, then 3, then 1 — one per bit — and as those frames pop in LIFO order they glue on "1", then "1", then "0" to spell "110", which is 6 in the base-2 you met back in chapter 0. The stack isn't an abstraction here; it's the thing assembling the digits.
the same tiny answer — recomputed until the machine drownsinteractive
one call — fib(n) — but how many times does the machine actually run fib() to answer it? fib(35) THE ANSWER fib(n) 9,227,465 TIME · ~100 ns / CALL 3.0 s ≈ a slow, awkward pause TIMES THE MACHINE RAN fib() FOR THAT ONE NUMBER 29.9M fib() calls · almost all repeats Cache each answer once and it collapses to 36 computed answers — a 829,464× cut. Same number, none of the repeats.
fib(35)
Naive fib(n) calls itself twice, so its frame tree branches — and each branch re-solves subproblems the other branch already solved. The depth is only n (the tower from Fig 3 never gets tall), but the breadth — the total number of calls — doubles roughly every step. Drag to fib(40) and the machine runs fib() a third of a billion times to hand back a nine-digit number it could have found in forty steps.
Fig 4 — Recursion’s hidden cost is not stack height, it is repeated work. The same fib(2) — a trivial 1+0 — is recomputed fib(n−1) times. This is exactly the waste that memoization (Chapter 12) erases by remembering each answer once, turning an exponential tree back into a straight line.
Trace3 step the machine — idea · code · memory move together
The call stack breathing — frames push on call, pop on return, value flows down
A running program is a self‑cleaning tower of frames: each call pushes a fresh sheet of private locals on top; each return pops it and lets its answer fall into the paused frame beneath — strict last‑in, first‑out. Press Next and watch the tower breathe.
stack · LIFOdepth 1→3→1total 445 s
The frame tower — newest on top, module at the basecall stack · LIFOrun
frames: 1
module
total_seconds()
song_seconds()
▲ newest frame on top · module frame at the base
In plain words
Under the hood
RUNALU · call & return
Program · playlist.py
in scope · type
Memory · the call stack
registers — the running state
frames top → bottom · each with its locals
what's happening
beat 1 / 1

05LEGB: where Python looks up a name

You type a name — plays, len, playlist — and, in that instant, Python has to decide which of the thousands of things in a running program you actually meant. It does not guess, and it does not search everywhere at once. It walks a fixed list of exactly four places, always in the same order, and stops the moment it finds a match. That list has a name you'll keep for life: LEGB.

Local — the frame of the function running right now. Enclosing — the names an outer def captured for a def nested inside it, held not in that outer frame (which may be long gone by the time the inner one runs) but in a shared cell the inner one can still reach. Global — the module: names defined at the top level of the file, like our playlist. Built-in — the names Python ships and you never had to write: len, print, range, sum. Four rooms, one order, first hit wins. Search all four and come up empty, and Python stops apologising and raises a NameError.

THE ONE IDEA TO CARRY FORWARD
A name is not a box you own — it's a question asked of four rooms, in a fixed order, answered by the first room that has it. Everything strange about scope, every bug in this section, is just this search running exactly as designed.

Reading names this way is quiet and intuitive — right up until the one sharp edge that catches every Python programmer exactly once. Here it is: the instant you assign to a name anywhere inside a function — the first line, the last line, even a line that never runs — that name becomes Local for the entire function, top to bottom. Watch it bite:

scope.pypython
plays = 0                      # a global name — the whole app's play count

def play_song(title):
    plays = plays + 1          # UnboundLocalError — not the error you expected
    print("now playing", title)

Read that and your gut says: "but plays is right there, it's 0, just add one." Python disagrees, and the reason makes sense once you see it. Python sorts every name into local-or-not when it compiles the def, before a single line has run. It scans the whole body, spots plays = …, and stamps plays as Local then and there. So at run time the read half of plays = plays + 1 doesn't fall through to the global. It looks in the function's own local slot — which is real, reserved, and still empty. Reaching into an empty local slot is exactly the UnboundLocalError you get. The global plays = 0 was never even consulted; the name had already been claimed by the local room.

Why the read fails: stamped local at compile time, empty at run timefigure
module: plays = 0 (never consulted below) ① COMPILE — scan the whole body first def play_song(title): plays = plays + 1 print(plays) locals = { plays } plays stamped LOCAL — for the whole function ② RUN — the read half reaches in first plays = plays + 1 read plays → look in the local slot local plays ∅ unbound UnboundLocalError the slot exists, but holds nothing yet
Fig 05 — The assignment doesn't just store a value later — at compile time it reclassifies the name. By the time the line runs, plays is a local slot that hasn't been filled, so the read on the right of = fails. The global plays = 0 sits untouched the whole time.

Once you know the cause, the two cures are clear. If you genuinely mean to rewrite the module's plays, say so with global — that keyword tells the compiler "don't stamp this local; the real one lives in the module." And when the name you mean to rewrite lives in an enclosing function rather than the module, there's a matching keyword for that too. You won't feel the need for it until closures start remembering things, two sections down, so we'll meet it there. Most of the time, though, the cleanest fix is to touch no outer name at all: take the value in, hand a new value back.

fixes.pypython
plays = 0

# option A — opt in to the global, and mean it
def bump_global():
    global plays
    plays = plays + 1          # after two calls, plays == 2

# option B — never touch the global; take it in, give it back
def next_count(plays):
    return plays + 1          # the caller decides where the result goes
global is a loaded weapon
A function that rewrites a global has invisible side effects: every caller now shares hidden state, and testing it means rebuilding the whole world around it first. Reach for global only when you truly must. Prefer to return the new value and let the caller decide where it lands — option B above has no spooky action at a distance.

Now let's watch the search itself run. Here's a tiny nested program — a play function tucked inside a make_player function, over a module-level playlist. Four different rooms hold the four names its body uses. Calling make_player(5) then play(200) returns 207, which is 200 + 5 + 2. Drag the control below and follow Python as it walks L → E → G → B, crossing off each room that misses until one hits.

player.pypython
playlist = [200, 245]             # G — "Blinding Lights", "Titanium"

def make_player(bonus):           # bonus lives in the ENCLOSING frame
    def play(seconds):            # seconds is LOCAL to play
        return seconds + bonus + len(playlist)
    return play
Look up a name — watch the LEGB search stop at the first hitinteractive
B · Built-in — len, print, range, sum … · G · Global (module) — playlist = [200, 245] · E · Enclosing (make_player) — bonus = 5 · L · Local (play frame) · seconds = 200 return seconds + bonus + len(playlist)
seconds
Python searches L → E → G → B and stops at the first hit; a crossed room () was searched and missed, a dim room was never reached. Four misses = NameError. Notice Global and Built-in compile to the same instruction — the deeper cut explains why.
Fig 05b — The rings are drawn nested because the search really is nested: it starts in the smallest room it's standing in and only widens outward. The green room is where the name lives; the tag beside the result is the actual bytecode Python emits to fetch it.
↺ reframe
Stop picturing a variable as a box with your value inside it. Picture a name as a lookup — a question Python re-asks every time the line runs, answered by the innermost room that happens to hold that name today. That's why the same word plays can mean the global on one line and a local on the next: nothing moved, the question just got a different first answer.
Wait — if Built-in is only the last room searched, what happens if I name a variable len? Exactly what LEGB predicts: your len is found in an earlier room and Python never reaches the built-in one. Write len = 999 at module level and the next len([200, 245]) dies with TypeError: 'int' object is not callable — you shadowed the tool with an integer. The built-in isn't gone; it's just been out-voted by a closer room. (del len removes your shadow and the real one answers again.)

✗ The myth

"Python hunts for a name everywhere — all my variables, imports, and built-ins — and somehow finds the right one."

✓ The reality

It checks exactly four places in one fixed order, L→E→G→B, and takes the first match without looking further. No global search, no cleverness — just a short, ordered walk that a beginner can run in their head.

The deeper cut — "Python searches four rooms" is a useful lie; here's the machinery

"Python searches four rooms at run time" is a labelled simplification — the right picture to reason with, and we'll refine it exactly once, here. In truth, only two of those rooms are ever searched while your program runs. When the compiler builds a function, it has already decided, per name, which room the name belongs to, and it bakes that decision into a different bytecode instruction for each scope. Disassemble our play and you can read the verdict directly: seconds compiles to LOAD_FAST (a Local — a numbered slot, no search at all), bonus to LOAD_DEREF (an Enclosing name, reached through a shared cell), and both playlist and len to LOAD_GLOBAL.

That last line is the twist worth keeping: Global and Built-in are the same instruction. LOAD_GLOBAL looks up the name in the module's dictionary first, and only if it misses does it fall through to the built-ins dictionary. So the "B" room isn't a separate search — it's the fallback of the "G" search. Local and Enclosing aren't dictionary searches at all; they were resolved to slots and cells before the function ever ran. The clean four-letter mnemonic LEGB is exactly right about order and outcome, which is all you need to predict every scoping bug. And now you also know the mechanism underneath it: two static bindings, then one dictionary lookup with a built-in safety net.

We keep saying "def just builds an object and binds a name." So play is a value like any other — you can hand the whole machine to someone else, not just the number it spits out. Next: functions are values you can pass around. →

THE ROOM YOU NEVER FURNISHED
The "B" in LEGB holds 158 names you never typed — len, print, sum, range and 144 other callable tools, plus 70 ready-made exception types like ValueError. Every one is found only after Local, Enclosing and Global have all missed. The room is always there, fully furnished and last in line.
Wait — a comprehension — that [i for i in range(3)] shorthand for building a list, which you'll meet properly next chapter — gets its own private scope. Run it and then ask for iNameError, it never leaked out. Yet a plain for i in range(3): pass leaves i sitting at 2 afterward. Same-looking loop, opposite result: the comprehension quietly runs in a room of its own.
WHY LOCALS ARE LITERALLY FASTER
Reading a local is not a search — the compiler turned it into a numbered slot, so LOAD_FAST is a plain array index. A global is a dictionary lookup by name. The gap is real and measurable, which is exactly why hot loops sometimes copy a global into a local first: same value, cheaper address.
Trace3 step the machine — idea · code · memory move together
LEGB in motion — one name, four rooms, first hit wins
Every name is a question asked of exactly four scopes in a fixed order — Local, Enclosing, Global, Built-in. Python stops the instant one room answers; miss all four and it raises NameError. Press Next and resolve each name in return seconds + bonus + len(playlist).
4 scopesorder L→E→G→B→ 207
The idea — four nested rooms, searched inside-outname lookupseconds
B · Built-inlen · print · sum · range …
G · Globalplaylist · make_player · play
E · Enclosingbonus = 5
L · Localseconds = 200
LOAD_FAST
In plain words
Under the hood
FASTALU · the lookup
Program · legb.py
variables · type
Memory · four scopes, searched L→E→G→B
registers — the running state
scopes four rooms · first hit wins · ✓ found / ✗ missed
what's happening
beat 1 / 1
Trace3 step the machine — idea · code · memory move together
Assign it anywhere, it's local everywhere — the UnboundLocalError trap
One assignment to plays inside the function makes plays local for the whole body — decided at compile time, before a line runs. So reading it first finds an empty local slot and raises UnboundLocalError, even though a module global plays = 0 sits right there. Press Next and watch the read miss the global entirely.
compile-time scopelocal UnboundLocalError
Where does the name plays resolve?compile → runmodule
LLocal · play_song()
EEnclosing · none here
GGlobal · moduleplays = 0
BBuilt-inlen · print
no frame yet
In plain words
Under the hood
OPALU · the operation
Program · scope.py
Memory · frames & the module
play_song frame · locals
module · globals
beat 1 / 1

06Functions are values: pass the machine, not its output

Everything you've handed a function so far has been data — a number, a string, a tuple of a song and its length. Now for the shift. In Python a function is itself just another value: an object like any other, with a type, an identity, and an address in RAM. You can drop it in a list, pin a second name on it, stash it as a value in a dict, or pass it to another function as an argument. Python's word for this is first-class — a function gets the exact same rights as a plain 200. And that one fact explains the mysterious key= you first met when you sorted a playlist back in chapter 6.

Say you want the playlist ordered by length, shortest first. You write the rule for what to sort by as an ordinary function, and then you hand that function to sort. The function itself, mind you, not a call to it:

sort_by.pypython
playlist = [("Blinding Lights", 200), ("Titanium", 245), ("bad guy", 194)]

def by_seconds(song):
    return song[1]                # pull out the length field

playlist.sort(key=by_seconds)      # hand over the machine — no parentheses
# playlist is now ordered shortest-first:
# [("bad guy", 194), ("Blinding Lights", 200), ("Titanium", 245)]

Notice what's missing: there are no parentheses after by_seconds. That absence is the entire idea, and it trips almost everyone the first time.

↺ reframe
The parentheses were never part of the name. by_seconds and by_seconds() are two different acts. by_seconds is a name that evaluates to the function object — the machine, sitting there cold, switched off. The () is a separate call operator that switches the machine on and evaluates to whatever it hands back. So key=by_seconds gives sort the machine to run once per song; key=by_seconds() would try to run it right now, at the call site — and blow up before sort has even started.
Hand sort the machine, or hand it the output?interactive
you write: playlist.sort( key = … ) key = by_seconds () the machine travels to sort, untouched the machine song → song[1] sort calls it once per song, then orders them ✓ [194, 200, 245] the () fires now by_seconds() it runs before sort, with no song to work on ✗ TypeError: missing 'song'
the machine (key=by_seconds)
Slide right to add the two characters that change everything: (). On the left you hand sort the machine to run per song; on the right the call fires first and sort is left holding nothing it can run.
Fig 06a — A bare name passes the function object; parentheses run it. Only one of these hands sort something it can call.
Wait — if a function is "just a value," what does Python actually keep under the name by_seconds? Ask it. type(by_seconds) answers <class 'function'>, and callable(by_seconds) is True while callable(200) is False. Assign a second name, metric = by_seconds, and metric is by_seconds comes back True — same object, two labels, one identity. A function is an object whose single party trick is that you're allowed to put () after it.

When the machine is a single expression and needs no name of its own, you can build it right at the call site with lambda. Read lambda song: song[1] straight across: lambda announces a tiny function, song is its one parameter, and whatever the expression after the colon works out to is returned automatically — no def, no return, no name. It's the very same kind of object by_seconds is, just born anonymous and thrown away after one use:

inline.pypython
# same order, machine built inline and forgotten after
playlist.sort(key=lambda song: song[1])

# a lambda is the SAME kind of object as a def'd function
f = lambda song: song[1]
type(f)              # <class 'function'>   — identical type
f.__name__           # '<lambda>'           — just born nameless
by_seconds.__name__  # 'by_seconds'

# because functions are values, a dict can be a menu of them
order_by = {"length": by_seconds, "title": lambda s: s[0]}
playlist.sort(key=order_by["length"])   # look up a machine by name, then hand it over

✗ The myth

A lambda is a stripped-down, second-class "mini function" — some lightweight cousin of a real def, with less power under the hood.

✓ The reality

It builds the exact same object — type(f) is function either way, verified above. The only real differences: a lambda has no name of its own (__name__ is '<lambda>') and its body must be a single expression. Same machine, born without a nameplate.

The deeper cut — what "run it now" actually does

"key=by_seconds() hands sort a single answer instead of a machine" is a useful half-truth — clean enough to steer by, so let's sharpen it. What really happens is that () fires before sort is ever called, and by_seconds() with no song raises TypeError: by_seconds() missing 1 required positional argument: 'song'. You never reach sort at all. And even where the inner call could succeed — picture a zero-argument function — sort would then be handed that call's return value (say the number 200) as its key, and choke a different way. key has to be callable, so sort tries to run 200(song) per item and hits TypeError: 'int' object is not callable. Two different explosions, one root cause: parentheses run the machine, a bare name passes it. That single distinction is the whole section in a line.

Here's a concrete problem. You want a plays() that returns 1, then 2, then 3 — a counter that remembers across calls. A plain local won't do it: every call gets a fresh frame, so the count resets to 0 each time. A module global works but leaks — every caller now shares one hidden number, spooky action at a distance. A whole class (chapter 12) is a lot of ceremony for a single integer. What you actually want is private state that lives between calls and belongs to no one else. Here is the move that gives it to you: define a function inside another function and return the inner one. The inner function keeps using a name from the outer function's frame, even after that frame has finished and popped. The compiler sees this coming at compile time and marks that name for capture. So instead of a plain frame slot, the value is born in a small heap-allocated box called a cell, which the inner function grips for as long as it lives. An inner function carrying captured variables like this is a closure, and nonlocal is the keyword that lets it rebind a captured name instead of accidentally shadowing it with a fresh local:

counter.pypython
def make_counter():
    count = 0                  # lives in make_counter's frame
    def bump():
        nonlocal count         # rebind the ENCLOSING count, don't make a new local
        count += 1
        return count
    return bump                # hand the inner machine out; the frame then pops

plays = make_counter()
plays()   # 1
plays()   # 2
plays()   # 3   — the count survived, with no global and no class
The frame is gone — the machine kept what it neededinteractive
make_counter() returned bump, then its frame popped — yet count lives on make_counter() frame POPPED — only its stack ref goes plays = the inner machine carries the cell everywhere it goes on the heap all along heap cell count = 0 closure grip plays() — not called yet press it again and the cell climbs a second make_counter() call: count = 0 its own private backpack — the slider never touches this one
0 calls
Each press runs bump(), which does count += 1 inside the surviving cell. The number climbs and stays climbed — that's state, with no global and no class in sight. The counter on the right proves each make_counter() gets its own cell.
Fig 06b — The captured count was born in a heap cell — the compiler saw the capture coming (make_counter.__code__.co_cellvars is ('count',)), so it never lived in the frame to begin with. When the frame pops it drops only its stack reference; the cell, gripped by the returned function, lives on. Private, persistent state — no class required.
A closure is a function with a backpack
It carries the enclosing variables it needs everywhere it goes. Every fresh make_counter() packs a new backpack, so two counters never share a count — call one and the other doesn't budge. It's the lightest way in the whole language to give a function private, persistent state; when that state grows into many fields and behaviours, you'll graduate to a class (chapter 12).
The deeper cut — the cell is real, and it's cheap

The backpack isn't a metaphor Python invents for teaching — it's a concrete object you can hold. After plays = make_counter(), the captured names live in plays.__closure__, a tuple of cells, and plays.__closure__[0].cell_contents is the live count. Nothing is really "rescued at the last second," either. The compiler spots the capture ahead of timemake_counter.__code__.co_cellvars is ('count',) and the inner plays.__code__.co_freevars is ('count',) — so count is stored in a heap cell from the very start, ready to outlive the frame. (An ordinary function that captures nothing has __closure__ equal to None.) Drop the nonlocal and Python treats count += 1 as a brand-new local, reads it before it's assigned, and raises UnboundLocalError; nonlocal is what aims the write back at the cell. And "lightest" is literal: that cell weighs sys.getsizeof ≈ 40 bytes on this Python, while an everyday object drags a per-instance __dict__ of a few hundred bytes more. (A class with __slots__ closes the gap, but you rarely reach for that until you're truly counting bytes.)

You can now hand a machine to sort. Next chapter you hand machines to map and filter, collapse whole loops into a single-line comprehension, and wire up sequences so lazy they compute each value only at the instant you ask for it — and barely touch memory doing it →

THREE CLOSURES, ONE SURPRISE
[lambda: i for i in range(3)] — a list comprehension, next chapter's tool; read it as 'build three functions, one per loop turn' — makes three functions that all return 2. A closure captures the variable, not its value, and by the time you call them the loop has long since left i parked at its final value. Every closure is reading the same cell, late. It is the most-reported "bug that is not a bug" in Python.
Wait — the closure's backpack is astonishingly light. The heap cell holding count weighs about 40 bytes; give the same one counter to a class instance and it drags a per-instance __dict__ of roughly 300 bytes more. For private, persistent state, a function-with-a-backpack is the leanest tool in the language — around 7× lighter than the class you would reach for by reflex.
Wait — "functions are values" includes Python's own. You can hand sort a built-in machine directly: words.sort(key=len) passes len itself — no parentheses — and sort runs it once per item. Check it: callable(len) is True, callable(200) is False. First-class is not a privilege reserved for functions you wrote.
Trace3 step the machine — idea · code · memory move together
A function with a backpack — the frame pops, the cell lives on
An inner function that keeps using an enclosing variable makes Python box that variable in a heap cell from the moment the outer function is compiled — the capture is decided ahead of time (co_cellvars), not rescued at the last instant. So when the outer frame pops it takes only the stack reference; the cell — private, persistent, with no global and no class — lives on. Press Next and watch the frame's reference drop while the cell keeps climbing.
closure · cellnonlocalcell · 40 B
The backpack — frame on the stack, cell on the heapclosurecell
plays() fired  ×0 STACK HEAP THE MACHINE make_counter() frame · on the stack holds: count, bump writes 0 heap cell · 40 B count = 0 closure grip bump() inner function closure just built ② a SECOND make_counter() — its own cell, proof of independence make_counter() #2 a fresh call its own cell count = 0 plays2 independent
In plain words
Under the hood
SETALU · the operation
Program · counter.py
in scope · type
Memory · heap cells
registers — the running state
heap · cells the boxed, captured state
what's happening — frame vs cell
Frame
Cell
beat 1 / 1
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 09, in working code

A function is a machine you give a name: feed it arguments, it runs its own private lines, and hands one value back. This chapter watches the line-marker step into those machines and back out again, and shows you the moment a caller's variables grey out because a function can never see them.

Naming a machine
def builds the machine, return hands a value back, and the caller catches it.
Passing arguments your way
Name them out of order, or gather however many arrive.
Where names live (scope)
Every call gets its own private drawer of names, invisible to everyone else.
Functions as values
A function is a thing you can return, store, and hand to other functions.
A machine that calls itself
Recursion stacks a fresh frame each time — then unwinds them one by one.
end of chapter 09 · six sections
iolinked.com
Written by Ajai Raj