Python Tutorial: Variable Scope - Understanding the LEGB rule and global/nonlocal statements
Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Python resolves names using LEGB: Local, Enclosing, Global, then Built-in, in that exact order.
Briefing
Python variable scope hinges on where a name is defined and the order Python searches when code references that name. The practical takeaway is the LEGB rule—Local, Enclosing, Global, Built-in—which determines what value a variable resolves to inside functions. Getting this wrong is a common source of “why is this value different?” bugs, especially when nested functions or assignments are involved.
In the tutorial’s first examples, a module-level variable X is set to “Global X,” while a function test defines a local variable Y. When Y is printed inside test, Python finds Y in the local scope first and outputs “local y.” But when Y is printed outside the function, Python can’t find it in any scope (local, enclosing, global, or built-in), so it raises a NameError. The contrast is deliberate: variables created inside a function don’t automatically exist after the function returns.
The behavior changes when the function assigns to a name that already exists globally. If test prints X, Python still resolves X from the global scope because X isn’t defined locally inside test. If test assigns to X without any special declaration, that assignment creates a new local X that shadows the global one; the global X remains unchanged outside the function. To actually modify the module-level X from inside test, the code must use the global keyword. With global X declared inside the function, assignments target the module variable rather than creating a local shadow. The tutorial also notes an important nuance: global declarations can be used even if the global name wasn’t previously defined at the top level, but only for the purpose of assigning that name.
Next comes built-in scope, which is the final fallback in LEGB. Names like min, print, int, list, and range are available without imports because they live in Python’s built-in namespace. The tutorial demonstrates how overriding built-ins can break expectations: defining a function named Min in the global scope causes Python to resolve Min locally/global-first, so calling Min may fail or behave differently than the built-in min.
Finally, the enclosing scope completes LEGB. Enclosing applies to nested functions: an inner function can access variables defined in any directly enclosing function’s local scope. If an inner function references X and doesn’t define X locally, Python searches outward to the enclosing function before checking global and built-ins. To modify a variable in an enclosing function rather than just read it, Python uses the nonlocal keyword. That’s the mechanism for changing closure state—useful for patterns like decorators and closures—without affecting the module-level global namespace.
By the end, the rule order and the distinction between reading vs assigning across scopes are clear: local variables are function-contained, global/nonlocal are explicit tools for cross-scope assignment, and built-ins are the last resort when a name isn’t found anywhere else.
Cornell Notes
Python resolves variable names using the LEGB rule: Local, Enclosing, Global, then Built-in. Names defined inside a function are local to that function; printing them outside triggers a NameError. Reading a module-level variable from inside a function works automatically, but assigning to that name inside the function creates a new local variable unless global is declared. For nested functions, enclosing scope lets an inner function read variables from its enclosing function, and nonlocal allows the inner function to modify that enclosing variable. Built-ins are the final fallback, and overriding built-in names in global scope can change behavior unexpectedly.
What does LEGB mean, and why does its order matter when a name is referenced inside a function?
Why does printing Y outside a function fail after Y is assigned inside that function?
How can a function read a global variable, and how does assignment differ from reading?
When should global be used, and what changes when global X is declared inside a function?
What’s the difference between enclosing scope and nonlocal in nested functions?
How can overriding built-ins like min lead to confusing errors?
Review Questions
- If a nested inner function references X but doesn’t define X locally, which scopes does Python check first under LEGB?
- What happens when a function assigns to a name that exists globally but does not declare global for that name?
- How do global and nonlocal differ in terms of which scope gets modified?
Key Points
- 1
Python resolves names using LEGB: Local, Enclosing, Global, then Built-in, in that exact order.
- 2
Variables assigned inside a function are local to that function; they are not accessible outside without explicit cross-scope handling.
- 3
A function can read a global variable without any declaration, as long as the name isn’t defined locally in that function.
- 4
Assigning to a global name inside a function creates a local shadow unless global is declared, leaving the module-level value unchanged.
- 5
To modify a module-level variable from inside a function, declare it with the global keyword.
- 6
Nested functions use enclosing scope for reads; use nonlocal to modify variables in an enclosing function’s local scope.
- 7
Overriding built-in names in global scope changes name resolution and can break expected behavior.