Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~26 min· topic 5 of 10

Topic 5.5

Scope and Namespaces: LEGB

In one line

Scope decides which names a piece of code can see. Python looks a name up in four places in order: Local, Enclosing, Global, Built-in (LEGB). Assigning to a name inside a function makes it local unless you declare it global or nonlocal.

Think of it like this

Rooms in a school. A note on your own desk (local) is read first. If it isn't there, you look at the classroom board (enclosing), then the school notice board (global), and last the national rule book that every school shares (built-in). Names in Python are looked up in that same order, from the nearest place outward.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Scope
The region of code where a name can be seen.
Namespace
A table that maps names to values.
Local
A name created inside the current function call.
Enclosing
A name from an outer function that contains the current one.
Global
A name defined at the top level of the file (the module).
Built-in
A name Python provides everywhere, such as len and print.
Shadowing
Reusing a name so it hides an outer one with the same name.
Rebinding
Pointing a name at a different value with = or +=.

Step by step

01Each function call gets private names

A variable made inside a function belongs to that call only. When the function returns, the names vanish. Outside code cannot see them, and a variable with the same name outside is a different variable.

main.pywhole filepython
x = "global"


def show():
    x = "local"          # a new local x, not the global one
    print("inside:", x)


show()
print("outside:", x)     # the global x is untouched

02LEGB: look near first

With nested functions there are more layers. Python searches inner to outer and stops at the first match. The built-in layer is last, which is why len works anywhere unless you shadowed it.

main.pywhole filepython
x = "global"


def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print("inner sees:", x)

    inner()
    print("outer sees:", x)


outer()
print("module sees:", x)
LEGB: look near firstdiagram
Rendering diagram…

03Reading a global is fine

A function can read a global name without any declaration, because lookup falls through Local and Enclosing to Global. The trouble starts only when the function assigns to the name.

main.pywhole filepython
rate = 0.18


def with_tax(price):
    return price * (1 + rate)     # reads the global rate


print(with_tax(100))

04Assignment makes a name local

In bump below, counter += 1 means counter = counter + 1. Because there is an assignment, Python marks counter as local to bump for the entire function. The right-hand side then reads a local that has no value yet, and the program stops with UnboundLocalError.

main.pywhole filepython
counter = 0


def bump():
    counter += 1    # counter is local here, with no value yet


bump()

05global and nonlocal

global count tells Python that count in this function means the module-level one, so += changes it. nonlocal total does the same for a variable of the enclosing function. Without these lines, assignment creates a new local. In make_adder, the inner function add is returned to the caller; Topic 5.6 and Topic 5.7 explain that idea. For now, just notice that total belongs to make_adder and add changes it.

main.pywhole filepython
count = 0


def bump():
    global count
    count += 1


def make_adder():
    total = 0

    def add(n):
        nonlocal total
        total += n
        return total

    return add

06Mutating is not rebinding

items.append(x) changes the list that the name points to. It does not rebind the name, so no declaration is needed even when items is global. This is also why a function that appends to a list passed in changes the caller's list (Topic 4.9).

07Shadowing a built-in

If you write sum = 100, the name sum in your file now points to an integer. Python finds it in the Global scope before it reaches Built-in, so sum([1, 2, 3]) tries to call 100. del sum removes your name and the built-in is visible again. The error message 'int' object is not callable means: you put round brackets after something that is not a function.

Try it yourself

  1. 1

    Predict which x

    In the first example, delete the line x = "local" in inner and run. Which value does inner print now? Then delete x = "enclosing" as well.

  2. 2

    Remove the declaration

    In the second example, delete the global count line from bump and run. Read the error, then put the line back.

  3. 3

    Mutate without a declaration

    Make a global list items = [] and a function add(x) that does items.append(x). Call it twice and print items. It works without global, because nothing was rebound.

Code & diagrams

LEGB in action New tab

Three different x values, one per scope. A function can read the global x, and len comes from the built-in scope.

Sign in to run this example in your browser.

Expected output

inner sees: local
outer sees: enclosing
module sees: global
read_only sees: global
a built-in: 3
global, nonlocal and shadowing New tab

The last lines shadow the built-in sum on purpose, then remove the shadow with del. The try/except lines catch the error so it can be printed (Topic 7.1).

Sign in to run this example in your browser.

Expected output

count: 2
5 15
TypeError: 'int' object is not callable
6
The four layersdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Change a global without declaring it

Add 1 to a global counter inside a function.

main.pywhole filepython
counter = 0


def bump():
    counter += 1


bump()
print(counter)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 8, in <module>
bump()
~~~~^^
File "main.py", line 5, in bump
counter += 1
^^^^^^^
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

Break #2

Shadow a built-in

Name a variable sum, then call sum as a function.

main.pywhole filepython
sum = 100
print(sum([1, 2, 3]))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(sum([1, 2, 3]))
~~~^^^^^^^^^^^
TypeError: 'int' object is not callable

Myth vs fact

Myth

A function cannot use a variable defined outside it.

Fact

It can read any visible outer name. It needs global or nonlocal only to rebind one.

Myth

global is needed to change a list defined outside.

Fact

Calling a method like append changes the object, not the name, so no declaration is needed.

Myth

Variables created in an if or for block are local to that block.

Fact

Python has no block scope. Only functions (and classes, modules, comprehensions) make a new scope.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Python decides local versus global at compile time, per function, by looking for assignments. That is why the error is UnboundLocalError even though the global of the same name exists. You can see the sets with func.__code__.co_varnames (locals) and co_names (globals and attributes).

  • ▸

    Local variable access is faster than global access in CPython, because locals live in an array indexed by position while globals need a dictionary lookup. Since 3.11 the interpreter caches global and built-in lookups, so the gap is small and is rarely a reason to change code. UnboundLocalError is a subclass of NameError, so except NameError catches both.

  • ▸

    Three scoping surprises worth knowing. (1) A class body is not an enclosing scope for the functions inside it: a method cannot see a class variable by its bare name, it must write self.name or ClassName.name (Phase 8). (2) The name in except SomeError as err: is deleted when the except block ends, so using err afterwards raises NameError. Copy it to another name if you need it later. (3) global x inside a function creates the module variable x on assignment if it did not exist, but nonlocal x is a SyntaxError unless an enclosing function already binds x.

  • ▸

    Comprehensions have their own scope: the loop variable of [x for x in data] does not leak into the surrounding code (Topic 4.7). A for loop's variable does leak and stays after the loop.

  • ▸

    Module-level state shared through global makes code hard to test and unsafe with threads and async tasks. Production code groups state in objects (Phase 8) or passes it explicitly.

Remember this

  1. 1

    A namespace is a table that maps names to values. Each function call gets its own local namespace for its parameters and for any name it assigns. The file itself has a global namespace (really the module's: a module is just a Python file, and Phase 6 teaches them). Python's own names such as len, print and sum live in the built-in namespace. A scope is the region of code where a namespace's names are visible.

  2. 2

    The lookup rule is LEGB. When code uses a name, Python searches the Local scope of the current function, then the Enclosing scopes of any functions that contain it, then the Global scope of the file, then the Built-in scope. The first match wins; if nothing matches, you get a NameError. This is why a variable named x inside a function does not disturb an x outside it.

  3. 3

    The key rule: assigning to a name anywhere in a function makes that name local to the whole function, decided when Python reads the def, before anything runs. Merely reading a name does not make it local, so a function can read a global freely. But counter += 1 is an assignment, so counter becomes local, and reading it before it has a value fails with UnboundLocalError.

  4. 4

    To change a global variable from inside a function, declare global name first. To change a variable of an enclosing function from a nested one, declare nonlocal name. Both are needed only for rebinding a name with = or +=. Changing the contents of a mutable object (items.append(x)) is not rebinding, so it needs neither.

  5. 5

    Using global often is a sign of design trouble: functions that secretly change shared variables are hard to test and to reason about. Prefer passing values in as arguments and sending results out with return (Topic 5.10). Constants (names in CAPITALS that never change) are the normal, harmless kind of global.

  6. 6

    Shadowing means reusing a name so that an outer meaning is hidden. Naming a variable list, sum, max, id or input shadows the built-in in that scope, and the next call to the built-in fails in confusing ways ('int' object is not callable). Pick other names, such as total or values. Also remember that function parameters are locals.

Explain it without notes

01

Explain the LEGB rule.

02

Why does counter += 1 inside a function raise UnboundLocalError?

03

What is the difference between global and nonlocal?

04

What is shadowing a built-in, and how do you avoid it?

Practice

01

Write a function show_scopes() with a local variable message = "local" and a global message = "global" defined above it. Print both the local (inside) and the global (outside).

02

Write make_total() that returns an inner function add(n) that adds to a running total and returns it, using nonlocal. Call it with 5 and then 10.

03

Fix this broken function by returning the new value instead of using global: score = 0 and def add_point(): score += 1.

Trade-offs

  • ↔

    Global state versus passing arguments. Globals are quick to write and hard to test; arguments and return values make data flow visible. Use globals only for constants and configuration set once.

  • ↔

    **nonlocal versus a small class.** nonlocal is neat for one counter or accumulator. With several related variables and methods, a class (Phase 8) is clearer.

Done when you can

  • Done when you can name the four scopes in order and give an example of each.

  • Done when you can explain why assignment makes a name local.

  • Done when you can fix an UnboundLocalError in two ways (global, or pass and return).

  • Done when you can use nonlocal in a nested function.

  • Done when you can recognise and undo the shadowing of a built-in.