Complex Scenarios¶
Mental Model
When closures, classes, and nested scopes interact, the key question is always the same: which namespace does this name resolve in, and when? Tracing the LEGB chain at call time -- not definition time -- reveals why late binding and nonlocal/global behave the way they do.
Nested Functions¶
1. Closure Binding¶
```python def outer(): x = 10
def inner():
return x # Binds to outer's x
return inner
f = outer() print(f()) # 10 ```
2. Multiple Levels¶
```python def level1(): x = 1
def level2():
x = 2
def level3():
return x # Binds to level2's x
return level3()
return level2()
print(level1()) # 2 ```
Class Attributes¶
1. Instance Binding¶
```python class MyClass: def init(self): self.x = 10 # Instance attribute
obj = MyClass() print(obj.x) # 10 ```
2. Class Binding¶
```python class MyClass: x = 10 # Class attribute
def __init__(self):
self.y = 20 # Instance attribute
obj = MyClass() print(obj.x) # 10 (class) print(obj.y) # 20 (instance) ```
Late Binding¶
1. Loop Problem¶
```python
Common mistake¶
funcs = [] for i in range(3): funcs.append(lambda: i)
All return 2!¶
print([f() for f in funcs]) # [2, 2, 2] ```
2. Solution¶
```python
Capture with default¶
funcs = [] for i in range(3): funcs.append(lambda x=i: x)
print([f() for f in funcs]) # [0, 1, 2] ```
Shadowing¶
1. Local Shadows Global¶
```python x = 10 # Global
def function(): x = 20 # Local shadows global print(x) # 20
function() print(x) # 10 ```
2. Parameter Shadowing¶
```python x = 10
def function(x): # Parameter shadows global print(x)
function(20) # 20 ```
Nonlocal Binding¶
1. Modify Enclosing¶
```python def outer(): x = 10
def inner():
nonlocal x
x = 20
inner()
print(x) # 20
outer() ```
2. Multiple Levels¶
```python def level1(): x = 1
def level2():
nonlocal x
x = 2
def level3():
nonlocal x
x = 3
level3()
level2()
print(x) # 3
level1() ```
Comprehensions¶
1. Own Scope¶
```python x = 10
Comprehension has own scope¶
result = [x for x in range(3)]
print(x) # 10 (unchanged) ```
2. Variable Leak¶
```python
In Python 2, leaked¶
In Python 3, doesn't leak¶
[i for i in range(3)]
print(i) # NameError in Python 3¶
```
Dynamic Binding¶
1. Runtime Creation¶
```python
Create binding at runtime¶
name = "x" value = 42
globals()[name] = value print(x) # 42 ```
2. exec()¶
```python
Dynamic code execution¶
code = "y = 100" exec(code)
print(y) # 100 ```
Summary¶
1. Complex Cases¶
- Closures
- Class attributes
- Late binding
- Shadowing
- Nonlocal
2. Best Practices¶
- Avoid shadowing
- Use defaults for loops
- Be explicit with nonlocal
- Limit dynamic binding
Exercises¶
Exercise 1.
Closures with nonlocal can modify enclosing variables. Predict the output:
```python def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment
c1 = counter() c2 = counter()
print(c1()) print(c1()) print(c2()) print(c1()) ```
Why do c1 and c2 maintain independent counts? What happens if you remove the nonlocal declaration?
Solution to Exercise 1
Output:
text
1
2
1
3
Each call to counter() creates a new frame with its own count = 0. The returned increment function captures that specific frame's count via a cell object. c1 and c2 point to different cell objects containing independent count variables.
Without nonlocal, count += 1 would raise UnboundLocalError. The += makes count a local variable (because it includes assignment), but the local count has no value yet when the += 1 tries to read it. nonlocal tells Python to use the enclosing scope's count instead of creating a new local.
Exercise 2. Class attribute vs instance attribute binding follows different lookup rules. Predict the output:
```python class Shared: data = []
a = Shared() b = Shared()
a.data.append(1) print(b.data)
a.data = [99] print(b.data) print(a.data) print(a.dict) print(b.dict) ```
Why does a.data.append(1) affect b.data, but a.data = [99] does not? What is the difference between mutating a class attribute and rebinding an instance attribute?
Solution to Exercise 2
Output:
text
[1]
[1]
[99]
{'data': [99]}
{}
a.data.append(1) mutates the class attribute Shared.data. Since a has no instance attribute named data, Python looks up the class and finds Shared.data. The .append() modifies this shared list in place, so b.data sees the change.
a.data = [99] creates an instance attribute on a that shadows the class attribute. Now a.__dict__ contains {'data': [99]}, while b still has no instance data and falls through to Shared.data (which is [1]).
The rule: attribute read walks instance → class → bases. Attribute write always writes to the instance (unless using descriptors). Mutation via method calls does not create a new binding.
Exercise 3. Name shadowing can cause subtle bugs. Predict the output:
```python x = "global"
def outer(): x = "enclosing" def inner(): print(x) inner()
def inner2():
print(x)
x = "local"
inner2()
outer() ```
Why does inner() work but inner2() raise UnboundLocalError? At what point does Python decide that x in inner2 is a local variable?
Solution to Exercise 3
inner() prints "enclosing" successfully. inner2() raises UnboundLocalError: local variable 'x' referenced before assignment.
Python's compiler (not the runtime) scans the entire function body at compile time. Because inner2 contains x = "local", the compiler marks x as a local variable in inner2's scope. This decision is made before any code runs. When print(x) executes, Python looks for x in the local scope (because the compiler said it's local) but finds it hasn't been assigned yet.
This is a fundamental aspect of Python's scoping: the scope of a variable is determined statically by the compiler based on assignment statements anywhere in the function body, not by the order of execution.