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
lenandprint. - 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.
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 untouched02LEGB: 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.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("module sees:", x)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.
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.
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.
count = 0
def bump():
global count
count += 1
def make_adder():
total = 0
def add(n):
nonlocal total
total += n
return total
return add06Mutating 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
Predict which x
In the first example, delete the line
x = "local"ininnerand run. Which value doesinnerprint now? Then deletex = "enclosing"as well. - 2
Remove the declaration
In the second example, delete the
global countline frombumpand run. Read the error, then put the line back. - 3
Mutate without a declaration
Make a global list
items = []and a functionadd(x)that doesitems.append(x). Call it twice and printitems. It works withoutglobal, because nothing was rebound.
Code & diagrams
Three different x values, one per scope. A function can read the global x, and len comes from the built-in scope.
Expected output
inner sees: local
outer sees: enclosing
module sees: global
read_only sees: global
a built-in: 3The 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).
Expected output
count: 2
5 15
TypeError: 'int' object is not callable
6Break 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.
counter = 0
def bump():
counter += 1
bump()
print(counter)Break #2
Shadow a built-in
Name a variable sum, then call sum as a function.
sum = 100
print(sum([1, 2, 3]))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
UnboundLocalErroreven though the global of the same name exists. You can see the sets withfunc.__code__.co_varnames(locals) andco_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.
UnboundLocalErroris a subclass ofNameError, soexcept NameErrorcatches 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.nameorClassName.name(Phase 8). (2) The name inexcept SomeError as err:is deleted when theexceptblock ends, so usingerrafterwards raisesNameError. Copy it to another name if you need it later. (3)global xinside a function creates the module variablexon assignment if it did not exist, butnonlocal xis aSyntaxErrorunless an enclosing function already bindsx. - ▸
Comprehensions have their own scope: the loop variable of
[x for x in data]does not leak into the surrounding code (Topic 4.7). Aforloop's variable does leak and stays after the loop. - ▸
Module-level state shared through
globalmakes 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
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,printandsumlive in the built-in namespace. A scope is the region of code where a namespace's names are visible. - 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 namedxinside a function does not disturb anxoutside it. - 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. Butcounter += 1is an assignment, socounterbecomes local, and reading it before it has a value fails withUnboundLocalError. - 4
To change a global variable from inside a function, declare
global namefirst. To change a variable of an enclosing function from a nested one, declarenonlocal 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
Using
globaloften 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 withreturn(Topic 5.10). Constants (names in CAPITALS that never change) are the normal, harmless kind of global. - 6
Shadowing means reusing a name so that an outer meaning is hidden. Naming a variable
list,sum,max,idorinputshadows 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 astotalorvalues. Also remember that function parameters are locals.
Explain it without notes
Explain the LEGB rule.
Why does counter += 1 inside a function raise UnboundLocalError?
What is the difference between global and nonlocal?
What is shadowing a built-in, and how do you avoid it?
Practice
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).
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.
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.
- ↔
**
nonlocalversus a small class.**nonlocalis 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
UnboundLocalErrorin two ways (global, or pass and return).Done when you can use
nonlocalin a nested function.Done when you can recognise and undo the shadowing of a built-in.