Functions · about 40 min

Scope: local, global and the LEGB rule

Why this matters

When you write x inside a function, which x do you get? The one you just assigned, one defined at the top of the file, or a built-in? The rules that answer this are called scope. Getting them right is what lets thousands of functions in a large program each use names like i, result and total without stepping on each other. Getting them wrong produces some of the most confusing errors beginners meet, especially UnboundLocalError. The rules are short and precise, so learn them once.

Namespaces: where names live

A namespace is a mapping from names to objects. Python has several, each with a lifetime:

You can look at two of them directly:

x = 1

def f():
    y = 2
    print("locals:", locals())

f()
print("x" in globals(), "y" in globals())
locals: {'y': 2}
True False

y existed only in f's local namespace, for the duration of the call.

The LEGB rule

When Python looks up a name to read it, it searches up to four scopes in order and uses the first match:

  L  Local      names assigned in the current function
  E  Enclosing  locals of any outer functions this one is nested in
  G  Global     names at the top level of the module
  B  Built-in   print, len, range, ...

If none has the name: NameError: name 'x' is not defined.

greeting = "hello"            # global

def outer():
    name = "Ada"              # local to outer, enclosing for inner
    def inner():
        punct = "!"           # local to inner
        return greeting + ", " + name + punct   # G, E, L
    return inner()

print(outer())
print(len(outer()))           # len found in Builtins
hello, Ada!
11

Assignment makes a name local

The rule for reading is LEGB. The rule for assigning is simpler and stricter:

Note: Any name assigned anywhere inside a function (by =, +=, a for loop variable, a def, an import) is local to that function, for the whole function body, unless declared global or nonlocal.

This decision is made by the compiler, when the function's bytecode is built, not while it runs. Disassembling shows it:

import dis
x = 10
def show():
    print(x)
def shadow():
    x = 5
    print(x)
dis.dis(show)
dis.dis(shadow)

In show the bytecode uses LOAD_GLOBAL x, while in shadow it uses STORE_FAST x and LOAD_FAST x. "Fast" locals are stored in numbered slots in the frame, not in a dictionary, which is one reason local variable access is quicker than global access.

So assigning a name inside a function never changes a global of the same name:

x = 10
def f():
    x = 99        # a new local x
f()
print(x)          # 10

UnboundLocalError

Combine "assignment makes it local everywhere in the function" with reading before assigning:

count = 0

def bump():
    count = count + 1
    return count

bump()
Traceback (most recent call last):
  File "demo.py", line 7, in <module>
    bump()
  File "demo.py", line 4, in bump
    count = count + 1
            ^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Because bump assigns count, the compiler made count local. The right-hand side reads the local, which has not been given a value yet. Python does not fall back to the global; the scope was fixed at compile time.

Changing globals: global (and why to avoid it)

The global declaration tells the compiler "this name refers to the module-level one":

count = 0

def bump():
    global count
    count = count + 1

bump(); bump()
print(count)     # 2

It works, but global mutable state makes programs hard to reason about and test: any function anywhere might change it, and the result of a call depends on history. The cleaner design passes state in and returns the new state:

def bump(count):
    return count + 1

count = 0
count = bump(count)
count = bump(count)
print(count)     # 2

Reading global constants is fine and common. By convention they are written in capitals: MAX_RETRIES = 3.

Enclosing scopes and nonlocal

Functions can be defined inside other functions. The inner function can read the outer function's locals (the E in LEGB), even after the outer function has returned:

def make_multiplier(factor):
    def multiply(n):
        return n * factor     # factor comes from the enclosing scope
    return multiply

triple = make_multiplier(3)
print(triple(10))   # 30

How can factor still exist after make_multiplier returned and its frame was discarded? CPython notices at compile time that factor is used by an inner function and stores it in a cell, a small object that the inner function keeps a reference to. The inner function together with its captured cells is called a closure. Module 13 explores closures in depth.

To assign to an enclosing variable, declare it nonlocal; otherwise the assignment would create a new local in the inner function:

def make_counter():
    count = 0
    def step():
        nonlocal count
        count += 1
        return count
    return step

c = make_counter()
print(c(), c(), c())   # 1 2 3

Shadowing built-ins

Since Global is searched before Built-in, a global named like a built-in shadows it:

list = [1, 2, 3]        # shadows the built-in list type
print(list("abc"))
TypeError: 'list' object is not callable

Common victims: list, dict, str, len, sum, max, min, input, id, type. Python does not warn you; choose different names (items, total, length). If you do it in the REPL, del list removes your global and the built-in reappears.

Loop variables and blocks

Unlike many languages, Python's if, for and while blocks do not create a scope. Only functions (and classes, comprehensions and modules) do:

for i in range(3):
    last = i * 10
print(i, last)   # 2 20  (still visible after the loop)

Common mistakes

Recap

Going further: read "Python Scopes and Namespaces" in the official tutorial (section 9.2). Try import builtins; print(dir(builtins)) to see every built-in name you might accidentally shadow.

Exercises

Exercise 1 · quiz

Which x?

What does this program print? Type the two printed values separated by a space.

x = 10

def change():
    x = 99
    return x

print(change(), x)
Answer

The assignment x = 99 inside change creates a local name x in the function's frame. It has nothing to do with the global x, which still refers to 10. Assignment inside a function always creates or rebinds a local name unless you declare otherwise.

Exercise 2 · quiz

UnboundLocalError

Why does calling bump() fail with UnboundLocalError: cannot access local variable 'count' where it is not associated with a value?

count = 0

def bump():
    count = count + 1
Answer
  • Because count is assigned somewhere in the function, the compiler made count local for the whole function, so the read on the right-hand side finds an empty local

Scope is decided at compile time, per function: any name assigned anywhere in the function body is local throughout that body. So count + 1 reads the local count, which has no value yet. Reading globals is fine when the function does not also assign them. The fix is to pass the value in and return the new one (best), or declare global count.

Exercise 3 · write code

Return instead of global

The starter uses a global variable to track a running total, and crashes. Rewrite add_to_total so it takes the current total and an amount and returns the new total, without any global statement or module-level variable:

add_to_total(0, 5)    # returns 5
add_to_total(5, 10)   # returns 15

File: solution.py

Starting point

total = 0

def add_to_total(amount):
    total = total + amount
    return total
Hint 1

Make the current total a parameter: def add_to_total(total, amount):.

Hint 2

The caller keeps the state: t = add_to_total(t, 5).

Reference solution
def add_to_total(total, amount):
    return total + amount

Exercise 4 · write code

nonlocal

Complete make_counter(). It returns an inner function step(); each call of step() adds 1 to a count kept in make_counter's scope and returns the new count. Separate counters must be independent:

a = make_counter()
b = make_counter()
a()   # 1
a()   # 2
b()   # 1

Use nonlocal, not global.

File: solution.py

Starting point

def make_counter():
    count = 0
    def step():
        count += 1
        return count
    return step
Hint 1

count += 1 assigns count, so Python treats it as local to step and fails.

Hint 2

Add nonlocal count as the first line of step.

Reference solution
def make_counter():
    count = 0
    def step():
        nonlocal count
        count += 1
        return count
    return step

Exercise 5 · write code

Who broke len?

This program crashes with TypeError: 'int' object is not callable. Find out why using the LEGB rule, and fix it by renaming the offending variable to length (do not remove any functionality). The fixed program must print exactly:

word length: 6
list length: 3

File: main.py

Starting point

word = "Python"
len = len(word)
print("word length:", len)
print("list length:", len("abc"))

Example

expected output

word length: 6
list length: 3
Hint 1

len = len(word) creates a global named len that is the integer 6.

Hint 2

Globals are searched before builtins, so the next len(...) finds the integer.

Reference solution
word = "Python"
length = len(word)
print("word length:", length)
print("list length:", len("abc"))