Output Puzzles: Scope, Closures in Loops and Class vs Instance Attributes
45 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)These puzzles are about where a name lives and when its value is read. A function looks names up when it runs, not when it is defined. A class attribute is shared, until an instance gets its own attribute with the same name.
The LEGB rule
The LEGB rule is the order Python searches for a name: Local scope, Enclosing function scopes, Global (module) scope, then Built-ins. The first match wins.
Python looks for a name in four places, in order: Local, Enclosing function, Global, then Built-in. Assigning to a name anywhere inside a function makes that name local for the whole function.
count = 0
def read(): return count
def bump(): count += 1
print(read())# bump() raises UnboundLocalErrorExample 1 · Which x is printed?
x = 1
def change(): x = 2
def show(): print(x)
change()show()x = 20show()1 20
What this code does
x = 2insidechangemakes a new local name, so the global stays 1.showreads the global when it runs, so it sees the new value 20 later.
read() only looks at count, so it finds the global and prints 0. In bump, the += makes count local, but it has no value yet, so the call fails.
Closures and nonlocal
A closure is a function that keeps access to variables from the function in which it was defined, even after that function has returned. It holds the variable, not a snapshot of its value.
A closure is an inner function that remembers variables of the outer function. It remembers the variable itself, not a copy of its value at that moment. To rebind such a variable, the inner function needs nonlocal.
def make_counter(): n = 0 def inc(): nonlocal n n += 1 return n return inc
a, b = make_counter(), make_counter()a(); a()print(a(), b())This prints 3 1. Each call to make_counter creates its own n, so a and b count separately.
Late binding in loops
A function reads a variable when it runs. Lambdas made in a loop all share the loop variable, so they all see its final value. A default argument is read when the lambda is created, which fixes this.
late = [lambda: i for i in range(3)]print([f() for f in late])fixed = [lambda i=i: i for i in range(3)]print([f() for f in fixed])Example 2 · Lambdas in a loop, three ways
late = [lambda: n for n in range(3)]default = [lambda n=n: n for n in range(3)]
def make(n): return lambda: n
factory = [make(n) for n in range(3)]print([f() for f in late])print([f() for f in default])print([f() for f in factory])[2, 2, 2] [0, 1, 2] [0, 1, 2]
What this code does
- The first list reads the shared
nafter the loop ends. - A default argument or a factory function captures the value at creation time.
This prints [2, 2, 2], then [0, 1, 2]. In the first list every lambda reads the same i, which ended at 2.
Class vs instance attributes
A class attribute is a variable defined in the class body and shared by every instance. Assigning the same name on one instance creates a separate instance attribute that hides it for that object only.
An attribute set in the class body is shared by all instances. Reading obj.name looks at the instance first, then the class. Assigning obj.name = ... always creates an instance attribute and does not touch the class.
class Account: rate = 5
a, b = Account(), Account()a.rate = 9Account.rate = 7print(a.rate, b.rate)
class Cart: items = []
x, y = Cart(), Cart()x.items.append("pen")print(y.items)Example 3 · Counting instances with a class attribute
class Counter: count = 0
def hit(self): self.count += 1
a, b = Counter(), Counter()a.hit()a.hit()b.hit()print(Counter.count, a.count, b.count)0 2 1
What this code does
self.count += 1reads the class value, adds 1, then stores the result on the instance.- The class attribute is never changed, so it stays 0.
Example 4 · Subclasses and class attributes
class Base: level = 1 tags = []
class Child(Base): pass
Child.level = 2Child.tags.append("x")print(Base.level, Child.level)print(Base.tags)1 2 ['x']
What this code does
- Assigning on
Childcreates a new attribute there;Base.levelstays 1. appendchanges the one list that both classes can see.
This prints 9 7, then ['pen']. a has its own rate, but x.items.append changes the one list shared by the class.
Next step
Check your understanding
12 quick questions