Level 2 · The Toolbox

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.

LetterScopeMeans
LLocalInside this function
EEnclosingInside the function that contains this one
GGlobalAt the top level of this file
BBuilt-inPython'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
LOGIC[Formidable: Success]

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]
🪤 Names to avoid

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.

Exercise 1

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.

Exercise 2

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)
15

The 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
15

The 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.

Exercise 3

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
175

There 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.

+100 XP