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.
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.
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 sLook 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.
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.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.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 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.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.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.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 labelBecause 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? →
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.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.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".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.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.
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)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.
245 lands in artist instead of seconds, and finally the one arrangement Python refuses to even compile.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.)
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)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.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.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.def add(song, playlist=None):
if playlist is None: # a brand-new list per call that needs one
playlist = []
playlist.append(song)
return playlistThe 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:
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-onlyAnd 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? →
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.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.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.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.
return, in one linefigurereturn, 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 ②).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:
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 backThat 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.
is, NOT ==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.
return, then no return line at all), then a tuple of two values — but it is always exactly one object.None.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:
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.
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 →
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.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.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.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.
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.
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.
song_seconds is pushed, run, and destroyed twice — the second call gets a brand-new frame, never the recycled first one.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.
fact(1) returns without calling again, and the tower unwinds, each level multiplying as its child returns. Keep pushing and Python eventually says stop."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.
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.)
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<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 differentn 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. →
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.RecursionError is not your program failing; it is the seat belt tightening a frame early.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.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.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.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.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.
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:
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.
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.
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 goesglobal 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.
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 playNameError. Notice Global and Built-in compile to the same instruction — the deeper cut explains why.plays can mean the global on one line and a local on the next: nothing moved, the question just got a different first answer.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. →
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.[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 i — NameError, 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.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.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:
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.
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.(). 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.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:
# 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:
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 classbump(), 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.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.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 time — make_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 →
[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.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.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.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.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.