Functions and structure
Scope, Lifetime, and the Mutable Default Trap
How Python resolves a name through four scopes, why a list default is shared across calls, and why a closure reads a loop variable after the loop has finished.
Two of the most reported Python bugs have the same shape: the value a function reads is not the value you expected, and the code that produced it looks correct. One is a list that keeps growing between calls. The other is a function that returns the last item of a loop instead of the item that was current when it was defined.
Neither is a quirk of the interpreter. Both follow from decisions that are documented and consistent: names are resolved by searching outward from where the code runs, and default values are evaluated when the function is created rather than when it is called. Learn those two rules properly and the bug class disappears.
Python searches four scopes, and it stops at the first match
When a function body reads a name, the interpreter searches four places in order: local, enclosing, global, builtins. The order is usually written as LEGB, and one example shows three of the four levels at once.
message = "global"
def outer():
message = "enclosing"
def inner():
print("inner sees:", message)
inner()
outer()
print("module sees:", message)
inner sees: enclosing
module sees: global
inner has no local message, so it looks in the enclosing
function scope and finds the one outer assigned. The module-level
message was never consulted, and it is untouched: the assignment inside
outer created a second name in a second namespace. That is all shadowing
means.
The fourth level is the builtins namespace, which is where print,
len and sum come from. It is searched last, which is why you can
shadow a built-in by accident and only find out later:
len = 3
print(len([1, 2, 3]))
Traceback (most recent call last):
File "shadow.py", line 2, in <module>
print(len([1, 2, 3]))
~~~^^^^^^^^^^^
TypeError: 'int' object is not callable
Module-level names are found before builtins, so the module's len wins and
the built-in function is unreachable for the rest of the program. Never name a variable
after a built-in you use. The same rule explains a list or
dict variable turning into a mysterious TypeError twenty lines
further down.
Assigning a name anywhere in a body makes it local everywhere in that body
Here is the failure everyone writes once:
count = 0
def record():
count = count + 1
return count
print(record())
Traceback (most recent call last):
File "bound.py", line 9, in <module>
print(record())
~~~~~~^^
File "bound.py", line 5, in record
count = count + 1
^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
The message reads as a contradiction: the name exists on line 1 and the function still
cannot access it. The explanation is that scope is decided when the body is compiled, not
when it runs. The compiler sees an assignment to count inside
record and marks that name local for the entire function. At run time, the
right-hand side reads the local count before anything has been assigned to
it, and Python does not fall back to the module.
So reading a global is always allowed, and assigning to one needs a declaration. That asymmetry is deliberate: it keeps a function that only reads a module-level constant from acquiring the ability to change it by accident.
global and nonlocal work, and both say something about your design
visits = 0
def record():
global visits
visits += 1
return visits
print(record(), record(), visits)
1 2 2
The declaration tells the compiler to bind visits in the module namespace
instead of the local one. It works, and in most code it is the wrong answer. The function
now depends on a name it does not own, any other module that imports this one can change
that name, and a test cannot run the function twice from a known state without resetting
it first. Passing the value in and returning the new one keeps the same behaviour and
moves the storage decision to the caller, who knows how long the value should live.
nonlocal is the narrower version, and it has a legitimate use: private
state that belongs to a closure and to nothing else.
def make_counter():
total = 0
def bump():
nonlocal total
total += 1
return total
return bump
counter = make_counter()
print(counter(), counter(), counter())
print("second counter starts fresh:", make_counter()())
1 2 3
second counter starts fresh: 1
The difference from global is reachability. Nothing outside
make_counter can name total, and every call to
make_counter gets a fresh cell, which is why the second counter starts at
1. Without the declaration, total += 1 raises the same
UnboundLocalError as record did, for the same reason.
My rule of thumb: one nonlocal declaring a counter, a cache or a
connection is fine and is how decorators work. Three of them in one function means the
state has grown into an object, and a class carrying that state will be easier to read and
to test. Module 4 covers that.
A mutable default is created once and shared by every call
The previous lesson established that a default value is computed when the
def runs. With a string, nothing visible happens. With a list, the same
object is handed to every call, and a function that modifies it modifies the default for
every future caller.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("draft"))
print(add_tag("review"))
print(add_tag("published"))
print("default object is now:", add_tag.__defaults__)
['draft']
['draft', 'review']
['draft', 'review', 'published']
default object is now: (['draft', 'review', 'published'],)
Three calls that pass one argument each, and the list grows. The empty list in the
signature was created once when the module was imported, stored in
__defaults__, and every call that omits tags receives that same
object. append mutates it in place, so the default is now the accumulator.
The last line is the proof and the diagnostic. Printing __defaults__
shows the stored value, and if it is anything other than empty, the function has been
mutating its own default. This is worth remembering when a function behaves differently
in a long-running process than it does in a short script: the state survived between
calls and nobody wrote it down.
The trap needs both halves. Defaults are evaluated once, and lists are mutable. Use an immutable default, or do not mutate the value you were given, and the signature is harmless.
The fix is a sentinel default, not a defensive copy in the caller
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
print(add_tag("draft"))
print(add_tag("review"))
print(add_tag("published", ["pinned"]))
print(add_tag("archived"))
['draft']
['review']
['pinned', 'published']
['archived']
None is a value no caller would pass to mean "a list of tags", so the body
can distinguish "nothing supplied" from "here is my list", and the new list is built
inside the call. The fourth line confirms the default is no longer carrying state.
The third line shows what the fix does not solve: a caller who passes a list still gets it modified. If that matters, stop mutating and return a new value instead.
def add_tag(tag, tags=None):
tags = [] if tags is None else list(tags)
return [*tags, tag]
existing = ["pinned"]
print(add_tag("published", existing))
print("caller's list:", existing)
['pinned', 'published']
caller's list: ['pinned']
This version copies the caller's list and returns a new one, so nothing outside the function changes. It costs a copy and it is the version I would rather review, because "this function does not modify what you hand it" is a property you can check once instead of at every call site.
A closure captures the variable, not the value it held
handlers = {}
for name in ("alpha", "beta", "gamma"):
def handler():
return name
handlers[name] = handler
print(handlers["alpha"](), handlers["beta"](), handlers["gamma"]())
gamma gamma gamma
Three function objects were created, each with its own __name__, and all
three return gamma. The body of handler does not store the
current value of name; it stores a reference to the variable. Name lookup
happens when the function is called, and by then the loop has finished and the variable
holds its last value. Every function shares one variable, so every function agrees.
This is late binding, and it is the intended behaviour for the case closures were built for: a function that reads the current state of something it was defined next to. It only surprises you when the variable is a loop variable that changed under the function.
The same shape appears in a list comprehension, where each lambda closes over the comprehension's own variable:
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])
[2, 2, 2]
Binding at definition time is the fix for late binding
handlers = {}
for name in ("alpha", "beta", "gamma"):
def handler(captured=name):
return captured
handlers[name] = handler
print(handlers["alpha"](), handlers["beta"](), handlers["gamma"]())
alpha beta gamma
The default argument is evaluated once per function object, at the moment the
def executes, so each handler walks away with its own copy of the name. The
rule that caused the mutable default trap is the rule that fixes this one. The parameter
exists only to hold the value, so name it something the reader will understand
(captured here) and do not accept it from callers.
Two other fixes are common. functools.partial binds arguments when it
builds the callable, which is the same idea with a library behind it, and a factory
function that takes the value as a parameter and returns the closure binds it in the
factory's scope. All three work; the default argument is the one that needs no import and
no extra function, which is why it shows up in library code.
Predict the output of this file before you run it. Write your prediction down, including the identity check.
def register(name, registry={}):
registry[name] = len(registry)
return registry
def make_getters(names):
return [lambda: n for n in names]
first = register("ana")
second = register("bo")
print(first, second)
print("same object:", first is second)
getters = make_getters(["ana", "bo", "cy"])
print([get() for get in getters])
Then fix both functions so the printed output is what a reasonable caller would
expect, without changing any call site. Explain, in terms of the two rules in this
lesson, why first is second is True in the original.
Show the solution and the reasoning
The original file prints this:
{'ana': 0, 'bo': 1} {'ana': 0, 'bo': 1}
same object: True
['cy', 'cy', 'cy']
first and second are the same dictionary, and the identity
check is what proves it. The first call bound registry to the default
object, returned it, and stored it in first. The second call received the
same object and added bo to it, so both names now show both entries. Note
that len(registry) produced the values 0 and 1,
which is the only reason the damage is legible: the second call saw a dictionary that
already had one entry. The getters return cy because all three lambdas
share the comprehension's n, and the comprehension finished before any of
them was called.
The fix keeps both signatures:
def register(name, registry=None):
if registry is None:
registry = {}
registry[name] = len(registry)
return registry
def make_getters(names):
return [lambda n=n: n for n in names]
first = register("ana")
second = register("bo")
print(first, second)
print("same object:", first is second)
print([get() for get in make_getters(["ana", "bo", "cy"])])
{'ana': 0} {'bo': 0}
same object: False
['ana', 'bo', 'cy']
In register, the None sentinel moves the creation of the
dictionary into the call, so each call that omits the argument gets a fresh one.
first is second becomes False and both dictionaries hold one
entry each, which is what the function's name promised.
In make_getters, the default parameter is evaluated once per lambda, so
each one captures the value of n at the moment it was created. That is the
same rule that produced the bug in register, used in the opposite
direction: a default is evaluated when the function object is built, and here that is
exactly the moment the value needs to be frozen.
The alternative fix for the getters is
[functools.partial(identity, n) for n in names] with
def identity(value): return value, which binds the argument at build time
without leaning on default-argument evaluation. Both are correct. The default argument
version needs no import, and the partial version reads as what it is
instead of looking like an unused parameter.
One thing the fix does not do is stop register from mutating a
dictionary the caller passes in. If that matters, copy at the top and return a new
dictionary, the way add_tag does above.
Progress
Mark this lesson done and it stays marked in this browser.