Scope, LEGB & closures
Local and global scope, the LEGB rule, UnboundLocalError, global/nonlocal and closures.
When you use a name like total, how does Python know which total you mean? The answer is scope: the region of code where a name is visible. Understanding scope explains a whole class of confusing bugs, and it unlocks closures — the mechanism behind decorators, callbacks and factories you'll use later.
Local and global scope#
A variable assigned inside a function is local to that function. It is created when the function is called and disappears when it returns. Variables assigned at the top level of a file are global to that module.
Trying to use y outside the function raises NameError: name 'y' is not defined.
Functions can read global variables without doing anything special:
Reading module-level constants like this is perfectly normal.
The LEGB rule#
When Python meets a name, it searches four scopes in order and uses the first match:
- Local — names assigned in the current function.
- Enclosing — names in any outer function(s), for nested functions.
- Global — names at the top level of the module.
- Built-in — Python's predefined names:
len,print,range,ValueError…
If no scope has the name, you get a NameError. This is also why shadowing built-ins is dangerous: a global named list or sum is found before the built-in.
The UnboundLocalError trap#
Python decides whether a name is local when the function is compiled, not line by line. If a function assigns to a name anywhere in its body, that name is local everywhere in the body:
Fix 1 (usually best): pass in, return out
Fix 2: the global statement
global works, but functions that secretly change globals are hard to test and reason about. Reserve it for rare cases like a module-level cache or configuration set once at start-up.
Mutating vs reassigning
You don't need global to mutate a global mutable object — only to reassign the name:
Nested functions and closures#
Functions can be defined inside other functions. The inner function can see the outer function's variables — and it keeps them alive even after the outer function has returned. That combination is called a closure.
Each call to make_multiplier creates a new factor and a new multiply that remembers it. Closures are a lightweight way to create configured functions — a "function factory".
nonlocal: changing enclosing variables
Just like global, reassigning an enclosing variable requires a declaration — nonlocal:
Each counter has its own private count that no outside code can touch — a tiny taste of encapsulation without classes.
The late-binding gotcha
Closures capture variables, not values. A closure looks up the variable's value when it's called:
Scope and blocks: a Python quirk#
Unlike C or Java, if, for, while and with blocks don't create a new scope. A variable assigned inside a loop is still available afterwards:
(Comprehensions do have their own scope, so the loop variable of [x for x in data] doesn't leak.)
Inspecting scopes#
locals() and globals() return dictionaries of the names currently in scope — handy for debugging and learning:
Worked example: a rate limiter using a closure#
The calls list lives in the enclosing scope, shared by every call to allow but hidden from the rest of the program.
Common mistakes#
UnboundLocalErrorfrom assigning to a global inside a function — pass and return values instead.- Overusing
global— it makes code hard to test. - Shadowing built-ins (
list,dict,id,input,sum). - Late binding in closures created in loops — capture the value with a default argument.
What's next#
That completes the beginner foundations! In the intermediate section we'll make functions more flexible with *args, **kwargs and lambdas.
Check your understanding
Quick quiz
1.In what order does Python look up a name (the LEGB rule)?
2.Why does this raise
UnboundLocalError?count = 0thendef inc(): count += 13.What keyword lets an inner function reassign a variable of its enclosing function?
Finished reading?
Mark this lesson complete to track your progress.