Scope: Where Names Live 🔭
Every name you create lives somewhere. Knowing where saves you from the two most confusing errors in Python: the variable that vanished, and the one that refused to change.
What happens inside stays inside
def make_grog():
strength = 11 # created inside the function
print(f"inside: {strength}")
make_grog()
# print(strength) # NameError: name 'strength' is not defined
print("outside: cannot see it")
inside: 11
outside: cannot see it
Names created inside a function are local to it. They come into
existence when the call starts and disappear when it ends. This is a feature: it means
you can name a variable total inside a function without wondering whether
some other function forty lines away is also using total.
Reading outwards is allowed
ship_name = "Sea Monkey" # global
def announce():
print(f"Now boarding the {ship_name}")
announce()
Now boarding the Sea Monkey
A function can read names from outside. Python looks in ever-widening circles, which has a name you will see in every Python book: LEGB.
| Letter | Scope | Means |
|---|---|---|
| L | Local | Inside this function |
| E | Enclosing | Inside the function that contains this one |
| G | Global | At the top level of this file |
| B | Built-in | Python's own names: print, len, str |
Python checks them in that order and stops at the first match.
Writing outwards is not
count = 0
def increment():
count = count + 1 # UnboundLocalError
try:
increment()
except UnboundLocalError as err:
print("Error:", err)
Error: cannot access local variable 'count' where it is not associated with a value
Read the rule that causes this, because it is not obvious: if a function assigns to a name anywhere in its body, Python treats that name as local for the whole function, including lines before the assignment.
So count = count + 1 tries to read a local count that does not exist yet. Not the global one. The decision was made when the function was compiled, before a single line ran.
You can override it, and you usually should not:
count = 0
def increment():
global count
count += 1
increment()
increment()
print(count)
2
Why global is usually the wrong answer
global works. It also means any function anywhere can change that value,
so when it holds something wrong you have the entire file as your list of suspects.
Prefer passing values in and returning them out:
def increment(count):
"""Return the next count. Changes nothing outside."""
return count + 1
count = 0
count = increment(count)
count = increment(count)
print(count)
2
Now the function is testable, reusable, and impossible to blame for an action at a distance. This is the shape of a pure function: it takes values, returns a value, and touches nothing else. Not everything can be pure, but the parts that can, should be.
Shadowing: same name, different scope
name = "global Guybrush"
def outer():
name = "outer Elaine"
def inner():
name = "inner Otis"
print("inner sees: ", name)
inner()
print("outer sees: ", name)
outer()
print("module sees:", name)
inner sees: inner Otis
outer sees: outer Elaine
module sees: global Guybrush
Three separate variables that happen to share a name. Each function sees its own. Legal, occasionally useful, and a reliable way to confuse yourself if you do it on purpose.
nonlocal: reaching one level out
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
tally = counter()
print(tally(), tally(), tally())
1 2 3
nonlocal means "the one in the enclosing function, not a new local and not
the global". The pattern above is a closure: increment
remembers count even after counter has finished. It is the
foundation of decorators (Lesson 35) and one of the genuinely beautiful ideas in
programming.
Shadowing built-ins: the sneaky one
list = [1, 2, 3] # now `list` is your list, not the type
print(list)
del list # undo the damage
print(list((1, 2, 3))) # the built-in is back
[1, 2, 3]
[1, 2, 3]
list, dict, set, str, int, type, id, sum, max, min, input, file, next. Python lets you use them as variables and then the real ones stop working, usually thirty lines later, with an error that makes no sense. Add an underscore: list_, or better, name it what it actually is: names.
Predict the scope
What does this print? Work it out before running.
x = 10
def show():
x = 20
print("inside:", x)
show()
print("outside:", x)Reveal solution
inside: 20 then outside: 10.
The assignment inside show created a brand new local x. The global one was never touched. If you wanted to change it you would need global x, and you almost certainly do not want to.
Fix the accumulator
This should total a list. It raises an error. Fix it two ways: once with global, once properly.
total = 0
def add(n):
total += n
add(5)Reveal solution
The quick fix:
total = 0
def add(n):
global total
total += n
add(5)
add(10)
print(total)
15The one you should ship:
def add(total, n):
"""Return the new total."""
return total + n
total = 0
total = add(total, 5)
total = add(total, 10)
print(total)
# or simply
print(sum([5, 10]))
15
15The second version can be tested in one line and cannot be broken by anything else in the program. And once it is written that way, you notice Python already has sum.
Build a bank account with a closure
Write a function that returns two functions: one to deposit and one to check the balance. The balance must not be reachable from outside except through them.
Reveal solution
def open_account(starting=0):
"""Return (deposit, balance) functions sharing a private balance."""
balance = starting
def deposit(amount):
nonlocal balance
balance += amount
return balance
def check():
return balance
return deposit, check
deposit, check = open_account(100)
print(check())
deposit(50)
deposit(25)
print(check())
100
175There is no way to reach balance from outside those two functions. That is encapsulation, achieved with nothing but scope. Classes (Lesson 31) are the more common way to do this, but closures got there first.