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, because there's a question most tutorials walk straight past. 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 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. We've climbed the LEGB ladder Python uses to resolve a name. And we've 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. That's the same three lines copied out thirty-nine needless times, every copy 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. They're labelled empty slots built into the machine's design, waiting. 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.
return. It runs for its effect — it prints, it draws, it saves. The call still evaluates to None.def line are parameters; the values at the call are arguments.return, and that changes everything. The call becomes a value you can name, add, print, or pass straight into another call.def line, the optional docstring, the indented body, and the return. Only the def line and one body line are compulsory.greet is the machine sitting cold. greet() is the machine running. The parentheses are a separate operator, not part of the name.you type
def greet():
print("Hey, how are you")
def describe(title, artist):
print(title, "by", artist)
def total_seconds(a, b):
"""Add two song lengths and hand the sum back."""
return a + b
greet()
describe("Titanium", "David Guetta ft. Sia")
total = total_seconds(200, 245)
print(total)
print(greet())
print(total_seconds(200, 245) + 100)
print(type(total_seconds).__name__, total_seconds.__name__)
print(total_seconds.__doc__)you see
Hey, how are you
Titanium by David Guetta ft. Sia
445
Hey, how are you
None
545
function total_seconds
Add two song lengths and hand the sum back.- The
defline ends in a colon and the body is indented. Miss either and the file will not run at all — syntax is checked before anything executes. - Writing
greetwhere you meantgreet()is legal and silent. You handed yourself the machine and never switched it on. - A Type 1 function hands back
None. Soprint(greet())prints the greeting, then printsNoneunderneath it. - The
defstatement must have run before the call does. Call a function above its owndefline and you getNameError. - Parameters are Ajai's word for the design, arguments for the delivery: "argument is the actual value of a parameter." Keep the two words apart and every error message below reads clearly.
And in memory, def does something you have already met. It builds a function object on the heap — the machine itself, which is the compiled body plus the list of parameter names. Then it binds the name describe to it, 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. 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, 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. It's a thread we pull hard on later in the chapter.
__doc__ for your code, help() for youhelp() shows first.__doc__, which is why tools can read it and a # comment can never be read back.help() has nothing to show.you type
def describe(title, artist, seconds):
"""Print one playlist line for a track.
title -- the song title, a str
artist -- who made it, a str
seconds -- how long it runs, an int
Returns None: this one prints, it does not hand anything back.
"""
print(title, "by", artist, "-", seconds, "s")
def shout(title):
"One-liner docstring: hand back the title in capitals."
return title.upper()
describe("Titanium", "David Guetta ft. Sia", 245)
print(shout.__doc__)
print(describe.__doc__.splitlines()[0])
help(shout)you see
Titanium by David Guetta ft. Sia - 245 s
One-liner docstring: hand back the title in capitals.
Print one playlist line for a track.
Help on function shout in module __main__:
shout(title)
One-liner docstring: hand back the title in capitals.
- The docstring must be the first statement in the body. Put one line of code above it and it becomes an ordinary string that Python evaluates and throws away.
- A
#comment is not a docstring. Comments are stripped at compile time; the docstring survives on the object andhelp()can find it. - No docstring means
name.__doc__isNone, soprint(f.__doc__.upper())raisesAttributeErroron theNone. help(shout)prints and returnsNone, exactly likeprint. Writingpage = help(shout)leavespageholding nothing.- Do not write
help(shout()). That calls the machine first — here it is aTypeErrorfor the missing argument, and you never reach the manual.
✗ 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, and 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 and wraps it in a fresh function object, stapling on the parameter defaults, the surrounding variables it can see, and the name describe. Then it 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. It 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 it 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. Write seconds=200 and it flies straight to the seconds slot no matter where you put it, order be damned. And 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.
f(b=2, a=1) lands the same as f(1, 2). The name travels with the value, so order stops mattering.SyntaxError otherwise.[] is evaluated once, when def runs, and stored on the function. Every default-taking call shares that one list.None is immutable, so sharing it is harmless, and if b is None builds a fresh list inside the call — where it belongs.you type
def add_song(title, artist="Unknown", seconds=180):
return f"{title} by {artist} - {seconds} s"
print(add_song("Blinding Lights", "The Weeknd", 200))
print(add_song("Blinding Lights", seconds=200, artist="The Weeknd"))
print(add_song("Titanium"))
print(add_song(artist="Sia", title="Titanium"))
try:
exec('add_song("Titanium", seconds=245, "Sia")')
except SyntaxError as e:
print("SyntaxError:", e.msg)
def add(song, playlist=[]): # the trap, run honestly
playlist.append(song)
return playlist
print(add("Blinding Lights"))
print(add("Titanium"))
print(add.__defaults__)
def add_safe(song, playlist=None): # the fix
if playlist is None:
playlist = []
playlist.append(song)
return playlist
print(add_safe("Blinding Lights"))
print(add_safe("Titanium"))you see
Blinding Lights by The Weeknd - 200 s
Blinding Lights by The Weeknd - 200 s
Titanium by Unknown - 180 s
Titanium by Sia - 180 s
SyntaxError: positional argument follows keyword argument
['Blinding Lights']
['Blinding Lights', 'Titanium']
(['Blinding Lights', 'Titanium'],)
['Blinding Lights']
['Titanium']- Read lines 6 and 7 of the output twice. The second
addcall returns two songs because both calls appended to the same list — the one built whendefran. add.__defaults__proves it from outside the function: the default is not an empty list any more, it is(['Blinding Lights', 'Titanium'],).- A positional after a keyword is a
SyntaxError, caught before the program starts. Nothing runs, not even the lines above it. def f(a=1, b)refuses too:SyntaxError: parameter without a default follows parameter with a default. Defaults go last, no exceptions.- Filling one slot twice —
add_song("Titanium", title="Levels")— raisesTypeError: add_song() got multiple values for argument 'title'.
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 overflow into a single collected object. *args scoops up every spare positional argument into a tuple. **kwargs scoops up every spare keyword argument into a dict. Those 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.)
*args is Ajai's XARGS: "packing the value to a tuple." Pass nothing and you still get a tuple — the empty one, ().**kwargs is his XXARGS: the keyword pairs arrive as a dict, so .items() from chapter 7 walks them straight away.*args, then keyword-only names, then **kwargs. Nothing may follow **kwargs.f(*row) spreads a 3-tuple across three slots exactly as if you had typed the three values.f(**meta) turns each key: value into key=value. It is how you hand a function a record you already had lying around.you type
def playlist_length(*songs):
print("packed:", songs, type(songs).__name__)
total = 0
for s in songs:
total += s
return total
def tag_song(title, **extras):
print("packed:", extras, type(extras).__name__)
for key, value in extras.items():
print(" ", key, "=", value)
def add_song(title, artist="Unknown", seconds=180):
return f"{title} by {artist} - {seconds} s"
print(playlist_length(200, 245, 354))
print(playlist_length())
tag_song("Blinding Lights", artist="The Weeknd", year=2019)
row = ("Titanium", "David Guetta ft. Sia", 245)
meta = {"artist": "Avicii", "seconds": 203}
lengths = [200, 245, 354]
print(add_song(*row))
print(add_song("Levels", **meta))
print(playlist_length(*lengths))you see
packed: (200, 245, 354) tuple
799
packed: () tuple
0
packed: {'artist': 'The Weeknd', 'year': 2019} dict
artist = The Weeknd
year = 2019
Titanium by David Guetta ft. Sia - 245 s
Levels by Avicii - 203 s
packed: (200, 245, 354) tuple
799playlist_length(lengths)without the star packs the list itself into the tuple:([200, 245, 354],), and the loop then tries to add a list to a number.- An empty
*argsis(), neverNone. Solen(songs)is0and theforloop simply runs zero times. - Every key in
**mappingmust be a string that matches a real parameter, or you getTypeError: got an unexpected keyword argument. - The names are yours.
*songsand**extraswork exactly like*argsand**kwargs— only the stars carry meaning. - Nothing may sit after
**kwargsin adefline. It is the last word in the signature, by design.
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)# make_track.py -- one constructor-style function, called four ways
def make_track(title, seconds, artist="Unknown", *, explicit=False):
"""Build one track record.
title, seconds -- required, usually passed by position
artist -- optional, defaults to "Unknown"
explicit -- keyword-only: after the bare *, it MUST be named
"""
return {"title": title, "seconds": seconds,
"artist": artist, "explicit": explicit}
def show(track):
mark = "E" if track["explicit"] else " "
minutes, seconds = divmod(track["seconds"], 60)
print(f" {mark} {track['title']:<16}{minutes}:{seconds:02d} {track['artist']}")
# 1 - everything by position, artist falls back to its default
one = make_track("Titanium", 245)
# 2 - all by keyword, in whatever order reads best
two = make_track(seconds=200, title="Blinding Lights", artist="The Weeknd")
# 3 - position for the required pair, name for the keyword-only flag
three = make_track("bad guy", 194, "Billie Eilish", explicit=True)
# 4 - built straight out of a dict the caller already had
row = {"title": "Levels", "seconds": 203, "artist": "Avicii"}
four = make_track(**row)
for track in (one, two, three, four):
show(track)
print()
print("defaults on the function:", make_track.__defaults__)
print("keyword-only defaults :", make_track.__kwdefaults__)
try:
make_track("One Dance", 173, "Drake", True)
except TypeError as e:
print("TypeError:", e)
Titanium 4:05 Unknown
Blinding Lights 3:20 The Weeknd
E bad guy 3:14 Billie Eilish
Levels 3:23 Avicii
defaults on the function: ('Unknown',)
keyword-only defaults : {'explicit': False}
TypeError: make_track() takes from 2 to 3 positional arguments but 4 were given
make_track.py and run it. One function, four call shapes, and the same kind of record falls out of each. Read the def line left to right, because the order is law. title and seconds carry no defaults, so they are required. artist carries one, so it has to sit after them. Then the bare * draws a line across the signature: everything after it is keyword-only, which is why call 3 must spell out explicit=True. Sit with call 4 for a moment. make_track(**row) takes a dict the caller already had and spreads it into three named arguments — the mirror of the gathering star, doing real work. The two lines near the bottom show where the machine keeps its fallbacks: ordinary defaults live in __defaults__, keyword-only ones in __kwdefaults__, both built once when def ran. Two things to try. Delete the bare * from the def line and watch the last call stop raising: True becomes just another positional value. Then read the TypeError as it stands — takes from 2 to 3 positional arguments but 4 were given is Python telling you, precisely, that the fourth value had nowhere to land.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 (callers may not name them); names after a lone * are keyword-only (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. Python stores them 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? →
def queue_song(song, queue=[]): queue.append(song); return queue. Call it three times in a row — queue_song("Blinding Lights"), then queue_song("Titanium"), then queue_song("bad guy") — and write down what you expect each call to print before you run anything. Now run it. The third call hands back all three songs, and you never asked it to remember. Your job has three parts. First, prove where the memory lives: print queue_song.__defaults__ after the calls and look at what is sitting in there. Second, repair it so each default-taking call gets a fresh list — and do it without changing any of the three call lines. Third, make sure your repair still lets a caller pass their own list in: queue_song("Faded", mine) must append to mine and nothing else. One hint and no more: the only object safe to use as a default is one that cannot be changed.show the solution
# the bug, run honestly
def queue_song(song, queue=[]):
queue.append(song)
return queue
print("call 1:", queue_song("Blinding Lights"))
print("call 2:", queue_song("Titanium"))
print("call 3:", queue_song("bad guy"))
print("stored on the function:", queue_song.__defaults__)
# the fix: None is the sentinel, the real list is built inside the call
def queue_song_fixed(song, queue=None):
if queue is None:
queue = []
queue.append(song)
return queue
print()
print("call 1:", queue_song_fixed("Blinding Lights"))
print("call 2:", queue_song_fixed("Titanium"))
print("call 3:", queue_song_fixed("bad guy"))
print("stored on the function:", queue_song_fixed.__defaults__)
# and it still lets a caller pass their own list in
mine = ["Levels"]
print("passed in :", queue_song_fixed("Faded", mine))
print("caller's :", mine)
# ------------- what it prints -------------
call 1: ['Blinding Lights']
call 2: ['Blinding Lights', 'Titanium']
call 3: ['Blinding Lights', 'Titanium', 'bad guy']
stored on the function: (['Blinding Lights', 'Titanium', 'bad guy'],)
call 1: ['Blinding Lights']
call 2: ['Titanium']
call 3: ['bad guy']
stored on the function: (None,)
passed in : ['Levels', 'Faded']
caller's : ['Levels', 'Faded']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 quiet magic. 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 did their job and dissolved. 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 is dead on arrival. The function has already left the building. That's not a rule to memorise so much as a consequence of the first job: you can't run more code after you've handed back your answer and gone.
Nonereturn is the early exit. It means "stop here, I have nothing worth handing over", and Python supplies None for you.return at all is the same thing again. The function still answers — it answers None, the one nothing-object.print shows a value to a human; return hands one to the program. A function that prints its answer gives you None to compute with.you type
def total(a, b):
return a + b
def clean(title):
if not title:
return # bare: nothing worth handing back
return title.strip().title()
def shout(title):
print(title.upper()) # prints, returns nothing
def longest():
return "Titanium", 245 # the comma packs ONE tuple
print(total(200, 245))
print(total(200, 245) + 100)
print(clean(" blinding lights "))
print(clean(""))
echoed = shout("levels")
print(echoed)
surprise = print("hi")
print(surprise)
pair = longest()
print(pair, type(pair).__name__)
title, seconds = longest()
print(title, seconds)
print(clean("") is None, shout("faded") is None)you see
445
545
Blinding Lights
None
LEVELS
None
hi
None
('Titanium', 245) tuple
Titanium 245
FADED
True Truex = print("hi")puts None inx.printdraws on the screen and hands back nothing — it is not a quietreturn.- A function that
prints its answer instead of returning it looks perfect on screen. Thentotal = shout(...) + 1raisesTypeErroron theNone. - Lines written after
returnon the same path never run. No warning, no error — the function was already gone. - An
ifwith noelsecan fall off the end and returnNoneby accident. That is the origin of most surprise-Nonebugs. - Check with
is None, not== None. There is only oneNonein the whole program, so identity is the honest question.
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. Then it gives you None the moment you try to do anything with the result.
# playlist_report.py -- BEFORE: one flat run of steps, nothing named
playlist = [("Blinding Lights", 200), ("Titanium", 245), ("bad guy", 194),
("Levels", 203), ("Faded", 212), ("One Dance", 173)]
total = 0
for title, seconds in playlist:
total = total + seconds
best_title = playlist[0][0]
best_seconds = playlist[0][1]
for title, seconds in playlist:
if seconds > best_seconds:
best_title = title
best_seconds = seconds
lines = []
lines.append("tracks : " + str(len(playlist)))
lines.append("total : " + str(total) + " s")
lines.append("longest: " + best_title + " (" + str(best_seconds) + " s)")
print("\n".join(lines))
# playlist_report.py -- AFTER: three named machines, each handing a value back
playlist = [("Blinding Lights", 200), ("Titanium", 245), ("bad guy", 194),
("Levels", 203), ("Faded", 212), ("One Dance", 173)]
def total_seconds(tracks):
"""Add every track length and hand the sum back."""
total = 0
for title, seconds in tracks:
total += seconds
return total
def longest(tracks):
"""Hand back the (title, seconds) pair with the biggest length."""
best = tracks[0]
for track in tracks:
if track[1] > best[1]:
best = track
return best
def report(tracks):
"""Build the three lines as one string -- print nothing."""
title, seconds = longest(tracks)
return "\n".join([
f"tracks : {len(tracks)}",
f"total : {total_seconds(tracks)} s",
f"longest: {title} ({seconds} s)",
])
print(report(playlist))
tracks : 6 total : 1227 s longest: Titanium (245 s)
total and best_seconds are loose names lying around the module, and the two loops are only distinguishable by reading every line of them. In the AFTER version each job has a name on the side, and the name says what it does before you read the body. Notice what none of the three functions do: none of them print. total_seconds hands back a number, longest hands back a tuple that the caller unpacks in one move, and report hands back a string. Printing happens once, at the very edge of the program, on the last line. That split is what makes the AFTER version testable — you can check total_seconds(playlist) == 1227 without a screen anywhere in sight. Two things to try. Add a seventh track and watch all three lines update with no other edit. Then change report to print its lines instead of returning them, and see how quickly you lose the ability to ask it a question.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. You 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 prettiest 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 manufactured one thing out of two. As for cost, that None is astonishingly 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 minting 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 →
total : 1227 s, which is honest and unreadable. A human wants 20:27. Write one function, mm_ss(seconds), that takes a whole number of seconds and returns the string — it must not print anything, because the caller decides where the text goes. Two operators do all the work, and you met both in chapter 4: // gives you the whole minutes, % gives you the leftover seconds. The one real trap is the zero. mm_ss(5) has to come back as "0:05", not "0:5", so the seconds always need two digits — look up the f-string spec :02d, which means "pad this number to width two with zeros". Check yourself against four inputs before you look: 1227, 245, 5 and 0. Then wire it into the type-along above so the total line reads 20:27, and print every track's length beside its title. Last question, and it is the one that matters: why does mm_ss return instead of print? Because a returned string can be printed, joined, or written to a file — and a printed one is already gone.show the solution
def mm_ss(seconds):
"""Format a whole number of seconds as M:SS."""
minutes = seconds // 60
rest = seconds % 60
return f"{minutes}:{rest:02d}"
print(mm_ss(1227))
print(mm_ss(245))
print(mm_ss(5))
print(mm_ss(0))
print(mm_ss(60))
print(mm_ss(3600))
playlist = [("Blinding Lights", 200), ("Titanium", 245), ("bad guy", 194),
("Levels", 203), ("Faded", 212), ("One Dance", 173)]
total = 0
for title, seconds in playlist:
total += seconds
print("total :", mm_ss(total))
for title, seconds in playlist:
print(f" {title:<16}{mm_ss(seconds)}")
# ------------- what it prints -------------
20:27
4:05
0:05
0:00
1:00
60:00
total : 20:27
Blinding Lights 3:20
Titanium 4:05
bad guy 3:14
Levels 3:23
Faded 3:32
One Dance 2:53None 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 it breathe, 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. That'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 quietly 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, 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. Watch each return pop one and hand its answer down. And watch the highlighted line track 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. 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, 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. 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 it slams into that ceiling just shy of the thousandth frame, where Python refuses to push another 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. It's 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. Popping it is moving that pointer back. No searching, no cleanup crew. That's precisely 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 rummage everywhere at once. It walks a fixed list of exactly four places, always in the same order, and stops dead 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. Those live not in the 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.
party() — so it sees party's names… right?party() calls report(), party's frame is sitting right beneath it — paused, alive, its names intact. Surely a function can read the variables of wherever it was called from; whoever presses GO shares their names with the machine.>>> mood = "calm"
>>> def report():
... print("mood is", mood)
...
>>> def party():
... mood = "hype" # the caller's own mood, alive on the stack
... report() # GO is pressed from INSIDE party
...
>>> party()
>>> del mood # now remove the module's mood entirely...
>>> party() # ...party's "hype" is STILL one sheet belowmood is calm Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in party File "<stdin>", line 2, in report NameError: name 'mood' is not defined
def was written — L→E→G→B, and the caller's frame is not one of the four — which is why report walked straight past party's "hype" to the module's "calm", then died rather than peek one sheet down the stack.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, a line that never even 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 is beautiful 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 obvious. If you genuinely mean to rewrite the module's plays, say so out loud 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. But most of the time the cleanest fix is to touch no outer name at all: take the value in, hand a new value back.
plays finds the module's copy through the normal L→E→G→B walk, no declaration required.plays = … anywhere in the body makes plays local everywhere in that body — decided at compile time, before a line runs.global cancels that claim and points the write at the module. It must appear before the assignment, and it is a promise you are changing shared state.nonlocal is the same move aimed one ring in: the name in the enclosing function, not the module. Without it the inner def just makes its own local.playlist.append(x) changes the object both names share, so it needs no keyword — and it reaches the caller anyway.you type
plays = 0
def show_plays():
print("read only, no assignment:", plays)
def bump_broken():
plays = plays + 1 # claims `plays` as local, so the read fails
def bump_global():
global plays
plays = plays + 1
def next_count(plays): # the clean fix: in through a slot, out via return
return plays + 1
def make_counter():
count = 0
def bump():
nonlocal count
count += 1
return count
return bump
show_plays()
try:
bump_broken()
except UnboundLocalError as e:
print("UnboundLocalError:", e)
bump_global()
bump_global()
print("after two bumps, plays =", plays)
print("next_count(7) =", next_count(7), "| global plays still", plays)
tick = make_counter()
print(tick(), tick(), tick())
print(len("Titanium"))you see
read only, no assignment: 0
UnboundLocalError: cannot access local variable 'plays' where it is not associated with a value
after two bumps, plays = 2
next_count(7) = 8 | global plays still 2
1 2 3
8plays = plays + 1with noglobalraises UnboundLocalError, notNameError. The local slot exists; it is simply still empty.- The claim covers the whole body. Even an assignment on the last line makes the name local on the first — the compiler read the body before you ran it.
globalafter the first assignment is aSyntaxError: name is assigned to before global declaration. Declare it at the top of the body.globalis a smell, not a sin. It hides state from every caller and makes the function untestable in isolation. Prefernext_count: take it in, hand it back.- Shadowing is legal and quiet. Name a local
lenand the built-in is unreachable in that function — Local is searched first, and first hit wins.
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 — that's 200 + 5 + 2. Drag the control below and follow Python's eyes 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 it belongs to. 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. →
3 min 20 s
4 min 5 s
Traceback (most recent call last):
File "C:\tmp\minutes.py", line 4, in <module>
total = song_minutes(200) + song_minutes(245)
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'+ was handed two Nones. song_minutes ends in print(...) with no return, so each call drew on the screen for a human and evaluated to None for the program. The traceback is really pointing at where the nothing was used — one line, sometimes a whole file, away from the function that forgot to hand its answer back.NoneType in an arithmetic error → hunt upstream for a function that prints instead of returningTraceback (most recent call last):
File "C:\tmp\lineup.py", line 4, in <module>
line = add_song("Blinding Lights")
^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: add_song() missing 2 required positional arguments: 'artist' and 'seconds'def add_song(title, artist, seconds) line before entering the machine, found two required slots still empty, and refused at the door. It is really reading the def line back to you — naming, in order, the exact slots that went unfilled.add_song("Blinding Lights", artist="The Weeknd", seconds=200) cannot land wrongTraceback (most recent call last):
File "C:\tmp\leftover.py", line 6, in <module>
print(total)
^^^^^
NameError: name 'total' is not definedtotal existed — for exactly one call. It was a local on total_seconds' frame, and the return popped that frame and wiped its names. The 445 crossed back as an object, but the call site — a bare total_seconds(200, 245) — never caught it, so by line 6 no name total exists anywhere. It is really saying: locals die with their frame; only the returned object survives the pop.total = total_seconds(200, 245) — the value survives the pop, the name never doesbonus = 10 and playlist = ["Titanium"]. Below them, four tiny functions. A is def a(): return bonus + 1. B is def b(): total = bonus + 1; bonus = 99; return total. C is def c(): bonus = 99; return bonus. D is def d(): playlist.append("Levels"); return len(playlist). Before you run a single line, write down four answers: what does each call return, and what are bonus and playlist afterwards? Take your time on B, because it is the one that catches everybody. The read of bonus happens on the line above the assignment, and your gut will insist that must be fine. It is not, and the reason is the whole section: the compiler scanned the body first and stamped bonus local for every line of it. Then ask the sharper pair of questions. Why does C assign to bonus and leave the module's 10 completely untouched? And why does D change the module's list without a global anywhere in sight, when C could not change a number? One word separates them, and it is not scope.show the solution
bonus = 10
playlist = ["Titanium"]
# A - reads a global it never assigns: the LEGB walk finds it, no keyword needed
def a():
return bonus + 1
# B - assigns bonus ANYWHERE, so bonus is local for the WHOLE body.
# The read on the line above the assignment hits an empty local slot.
def b():
total = bonus + 1
bonus = 99
return total
# C - a local named bonus, born and dead inside the call. The module's 10 never moves.
def c():
bonus = 99
return bonus
# D - no assignment at all: it MUTATES the object both names share.
def d():
playlist.append("Levels")
return len(playlist)
print("A:", a())
try:
print("B:", b())
except UnboundLocalError as e:
print("B: UnboundLocalError:", e)
print("C:", c(), "| module bonus still", bonus)
print("D:", d(), "| module playlist now", playlist)
# ------------- what it prints -------------
A: 11
B: UnboundLocalError: cannot access local variable 'bonus' where it is not associated with a value
C: 99 | module bonus still 10
D: 2 | module playlist now ['Titanium', 'Levels']
# The word that separates C from D is ASSIGNMENT.
# C assigns: the name is claimed as local, and the module's name is untouched.
# D mutates: no name is claimed, so it edits the one object the caller can see.
# To make B work, choose one: `global bonus` (and mean it), or pass bonus in
# as a parameter and return the new value. The second has no hidden state.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 watch the floor tilt. 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 post 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 quietly 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. 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: no parentheses after by_seconds. That absence is the entire idea, and it trips almost everyone the first time.
() and you hand over its answer instead — usually an error, always the wrong thing.key= is called once per item and the return value is what gets compared. The items themselves are never modified.lambda is a def with the ceremony stripped off: one expression, returned automatically, no name. Full treatment in chapter 10.type(f) is function for both; only __name__ differs, and the lambda's is literally <lambda>.key=lambda s: -s[1] sorts longest first by comparing negated lengths.you type
playlist = [("Blinding Lights", 200), ("Titanium", 245), ("bad guy", 194)]
def by_seconds(song):
return song[1]
print(sorted(playlist, key=by_seconds))
print(sorted(playlist, key=lambda song: song[1]))
print(sorted(playlist, key=lambda song: song[0]))
print(sorted(playlist, key=lambda song: song[0].lower()))
print(sorted(playlist, key=lambda song: -song[1]))
print(max(playlist, key=lambda song: song[1]))
print(sorted(["Levels", "bad guy", "One Dance"], key=len))
f = lambda song: song[1]
print(type(f).__name__, f.__name__, by_seconds.__name__)
print(f(("Faded", 212)))
try:
sorted(playlist, key=by_seconds())
except TypeError as e:
print("TypeError:", e)you see
[('bad guy', 194), ('Blinding Lights', 200), ('Titanium', 245)]
[('bad guy', 194), ('Blinding Lights', 200), ('Titanium', 245)]
[('Blinding Lights', 200), ('Titanium', 245), ('bad guy', 194)]
[('bad guy', 194), ('Blinding Lights', 200), ('Titanium', 245)]
[('Titanium', 245), ('Blinding Lights', 200), ('bad guy', 194)]
('Titanium', 245)
['Levels', 'bad guy', 'One Dance']
function <lambda> by_seconds
212
TypeError: by_seconds() missing 1 required positional argument: 'song'key=by_seconds()runs the machine now, before sort is even entered, so you get aTypeErrorabout a missing argument and never reach the sorting.- Lines 3 and 4 of the output differ, and that is not a bug. Raw title order puts every capital before every lowercase letter, so
"bad guy"lands last until you add.lower(). - A
lambdaholds one expression, never a statement. Noreturn, noifblock, no loop — the moment you want those, write adef. - Naming a lambda (
f = lambda s: s[1]) works but wastes the point of it. Its__name__stays<lambda>, which is what your traceback will show you. key=takes any callable, including Python's own:key=lenpasses the built-in machine itself — still no parentheses.
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 comes 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 to the metal. 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 in the case 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. It chokes 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 itch. 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. It 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. The nonlocal keyword 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 →
# dice_roll.py -- Ajai's Dice Roll Simulation, rebuilt so the functions are the star
import random
def roll(sides=6):
"""One roll of one die: a whole number from 1 to sides, inclusive."""
return random.randint(1, sides)
def simulate(rolls, sides=6):
"""Roll `rolls` times. Hand back a dict of face -> how often it came up."""
counts = {face: 0 for face in range(1, sides + 1)}
for _ in range(rolls):
counts[roll(sides)] += 1
return counts
def summarize(counts):
"""Turn the counts into report lines. Prints nothing -- it returns."""
rolls = sum(counts.values())
lines = []
for face, times in counts.items():
bar = "#" * round(times / rolls * 60)
lines.append(f" {face} {bar:<14}{times:>4} {times / rolls:6.1%}")
top = max(counts, key=counts.get)
lines.append(f" {rolls} rolls | most common face: {top} ({counts[top]} times)")
return lines
random.seed(9) # same seed, same run -- so your screen matches this page
print("one roll :", roll())
print("one roll :", roll())
print("one d20 :", roll(20))
print()
counts = simulate(600)
for line in summarize(counts):
print(line)
one roll : 4 one roll : 5 one d20 : 12 1 ############ 125 20.8% 2 ########## 103 17.2% 3 ######### 90 15.0% 4 ######### 92 15.3% 5 ########### 106 17.7% 6 ######## 84 14.0% 600 rolls | most common face: 1 (125 times)
roll_dice() that returns a number, and a main() that loops until you stop — and we keep it, then push the split one step further. Three machines, three jobs, and each one hands something back. roll knows about one die and nothing else; its sides=6 default is what lets the same function roll a d20 on the third line without a word of new code. simulate knows about repetition: it calls roll six hundred times and returns a dict of face → count, the chapter-7 container doing exactly the job it was built for. summarize knows about words, and notice that it prints nothing at all — it returns a list of lines and lets the caller decide. That is the same discipline as the report program: compute in the middle, print at the edge. The one line doing quiet heavy lifting is random.seed(9). It fixes the generator's starting state, so the numbers stay random-looking but come out identical every run — which is how the output above can be printed on a page at all. Two things to try. Delete the seed line and run it three times: the shape holds, the digits never repeat. Then call simulate(600, sides=20) and watch the bars flatten out, because twenty faces splitting six hundred rolls leaves each one thinner.[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 out again — and shows you the moment a caller's variables grey out because a function can never see them.
defis two quiet acts — build the machine, bind the name — and the body sleeps, compiled and shelved, until a pair of parentheses presses GO.- Arguments fill slots by position, by name, or by a default — and a default is built once, when
defruns, which is why a mutable one quietly remembers across calls. - Exactly one object crosses back —
returnends the call in the same breath, a comma packs a tuple, and a function that never speaks still answersNone. - Every call pushes a private frame of locals and every return pops one — a traceback is that stack, photographed at the moment it broke.
- A name is a question asked of four rooms in one fixed order — L→E→G→B, wrapped around where the
defwas written — and the machine itself is a value: hand it over bare, no parentheses.
def is one more binding; chapter 6 built the tuple that return a, b packs and *args gathers; chapter 7 opened the dict that **kwargs arrives in; chapter 8 stacked the plates that turned out to be the call stack itself. This chapter only wired them into machines with names.