Every name in a Python program (every variable, function, parameter, imported module) lives in some scope, which is just the region of code where that name is visible. When you write print(total), Python has to figure out which total you mean, because the same word could refer to a local variable, a module-level value, or even a built-in. The rule it uses is called LEGB, and once you know it, a whole class of "where did this name come from?" and "why does my function suddenly fail with UnboundLocalError?" questions stop being mysterious. This lesson covers the four scopes, the order Python searches them, name shadowing, the assignment-makes-it-local trap, and how to inspect scopes at runtime.
A scope is the part of your code where a name is visible. Two pieces of code can both define a variable called total without colliding, because each total lives in its own scope.
Both functions have a local variable named total, and they don't interfere with each other. Each call to either function creates a fresh scope, runs the function body, and then throws that scope away. The total inside cart_total is invisible from discount_total, and neither one leaks back out to the module level.
TAX_RATE is different. It's defined at the top of the file, outside any function, so it lives in the module scope and is visible to every function in the file. Both functions can read it without doing anything special.
A name being "visible" means two things: you can read it (use it in an expression) and you can call methods on it. It does not automatically mean you can rebind it. Rebinding a name that lives in an outer scope from inside a function is a separate topic that needs the global or nonlocal keyword.
Python has exactly four kinds of scope, and they're searched in a fixed order whenever you reference a name. The acronym is LEGB:
print, len, range, sum, int, True. These live in the builtins module.Here's each scope in one snippet, layered:
Look at the inner function format_line. Inside it:
line, label, and amount are local (parameters and a local variable).prefix is in the enclosing scope (the local scope of make_invoice_formatter).TAX_RATE would be global if we used it here.f (the f-string mechanism) is syntax, not a name. But print and len are names that come from the built-in scope.Enclosing scope only exists when functions are nested. In a flat module with no nested functions, the LEGB lookup effectively reduces to LGB, because there's no enclosing function to search.
The diagram shows the search order. Python walks outward from the most specific scope to the most general, stops at the first match, and uses that binding. If it walks all the way out to built-ins and still hasn't found the name, it raises NameError.
Notice the order is fixed. Python never "guesses" which scope you meant. It always starts local and walks outward. This is why a local variable can hide an outer one even when you didn't intend to (covered in the next section).
The mental model worth holding is: every time a name is referenced, Python searches the scopes in LEGB order and uses the first binding it finds. Let's watch this happen on a small E-Commerce snippet.
When the function runs, here's what each name reference triggers:
sum: Local? no. Enclosing? none. Global? no. Built-in? yes. Use the built-in sum.prices: Local? yes (it's a parameter). Use the local binding.subtotal: Local? yes (assigned in the previous line). Use the local binding.TAX_RATE: Local? no. Enclosing? none. Global? yes. Use the module-level value.The same name doesn't have to resolve to the same scope every time the function runs. If the module-level TAX_RATE changed between two calls, the function would see whatever value was in the global scope at the moment of the call. The lookup is dynamic in the sense that it happens at call time, not when the function was defined.
What if a name isn't anywhere? Python raises NameError:
Python searched local (not there), enclosing (none), global (not there), built-in (not there), gave up, and raised. The error message is specific: it tells you which name failed.
A subtle point: the error happens at call time, not when the function was defined. Python doesn't pre-check that every name in a function body exists. The function body is just code that runs when the function is called, and name lookups happen as each line executes. This is why you can define a function that references a name that doesn't exist yet, as long as the name does exist by the time the function actually runs:
The function was defined before shipping_fee existed, but by the time we called it, shipping_fee was a module-level name, so the lookup succeeded.
A second small trace makes this concrete. Suppose the file looks like this:
Inside grand_total, every name reference walks LEGB:
sum is not local, not enclosing, not global. Built-in: yes.prices is local (parameter).subtotal is local (assigned on the previous line).TAX_RATE is not local, no enclosing function, found in global scope.tax is local.SHIPPING is not local, no enclosing function, found in global scope.Every lookup terminates at exactly one scope, and the function gets one binding per name. None of the names overlap, so nothing is shadowed and the lookup is straightforward. The next two sections cover what happens when names do overlap, which is where the rule starts to bite.
A local variable with the same name as an outer one hides the outer one for the rest of the function body. This is called shadowing. The outer name isn't deleted or modified; it's just invisible from inside the function, because the local binding is found first in the LEGB walk.
Inside the function, TAX_RATE is the local 0.05, because the local binding is the first thing LEGB finds. Outside the function, TAX_RATE is still 0.08, because the module-level variable was never touched. The function created its own TAX_RATE in its own scope; the outer one is untouched.
Shadowing happens with parameters too. A parameter is a local name, and if it shares a name with an outer one, the parameter wins inside the function:
Same idea: inside the function, TAX_RATE refers to the parameter, not the module-level value. Outside, the global TAX_RATE is unaffected.
Shadowing built-ins is also legal and is one of the easiest mistakes to make. Naming a local variable list, sum, len, or dict shadows the built-in for that function, which usually isn't what you want.
The code runs, but inside cart_summary, the name list no longer refers to the built-in type. If you tried to do list("hello") later in the same function, it would fail because the local list is now a list object, not the type. The fix is to pick a different name (names, formatted, result) so the built-in stays available.
Cost: Shadowing built-ins doesn't slow code down, but it's a maintenance hazard. A reader scanning the function has to keep in mind that list doesn't mean what they think it means. Pick descriptive local names and the problem goes away.
The flip side of shadowing is also worth holding in mind. Inside a function, you can read a global variable without doing anything special, as long as the name isn't also assigned locally. As soon as you assign to that name anywhere in the function body, the name becomes local for the whole body, which is the topic of the next section.
This is the most surprising rule in Python's scope model, and it catches almost everyone the first time. If a name is assigned anywhere inside a function body, Python treats it as local for the entire body, even before the assignment line runs. That's not a typo; the assignment doesn't have to have happened yet for the name to count as local. The presence of the assignment statement is enough.
Here's the trap. The intent is to read the global TAX_RATE, multiply by it, and then update a local running total. The code looks reasonable:
The error fires on the print(TAX_RATE) line, which is before the assignment. Why? Because Python looked at the function body at definition time, saw the line TAX_RATE = TAX_RATE * 1.0, and decided "this function has a local variable called TAX_RATE". From that decision on, every reference to TAX_RATE in the function refers to the local one. The local one doesn't have a value yet when print(TAX_RATE) runs, so Python raises UnboundLocalError.
The fact that the global TAX_RATE exists doesn't help. Once Python has decided a name is local in a given function, LEGB only looks at the local binding. The global is invisible.
The same pattern shows up with +=, which is read-then-write:
TAX_RATE += 0.01 is shorthand for TAX_RATE = TAX_RATE + 0.01. The assignment makes TAX_RATE local, the read on the right side tries to use the local before it has a value, and you get UnboundLocalError.
The error message changed slightly between Python versions, but the meaning is the same: "you told me this is a local name, and you tried to use it before giving it a value." Older versions phrased it as local variable 'TAX_RATE' referenced before assignment. Both messages point at the same situation.
There are three ways out of this trap, depending on what you actually meant:
Now TAX_RATE is only read, never assigned, so it stays global. adjusted_rate is a brand-new local name with no conflict.
global keyword.nonlocal keyword.For now, the rule to internalize: if you assign to a name anywhere in a function, that name is local for the whole function, regardless of where the assignment sits.
Python gives you two built-in functions that let you peek into the current scope: locals() returns the local namespace as a dictionary, and globals() returns the module-level namespace. They're useful for debugging, teaching, and the occasional metaprogramming trick.
locals() shows the four names that exist in the function's local scope at the moment it was called: the two parameters and the two locally assigned variables. TAX_RATE and SHIPPING aren't in the local namespace, because they were only read, never assigned.
globals() returns the module-level dictionary, which holds every top-level name in the file (including imports, function definitions, and module-level constants). It's large enough that printing all of it isn't usually useful; you'd typically index into it for one name.
The first two reads work because both names exist at the module level. The third check returns False because we haven't defined cart_total in this snippet. If we had, it would be there: every top-level def adds the function to the module's globals.
A handy thing about globals() is that it returns the actual module namespace, not a copy. Mutating the dict mutates the module's globals. This is mostly a curiosity (and a footgun), but it does mean globals()["TAX_RATE"] = 0.10 would change the module-level TAX_RATE. Reading is fine and common; writing through globals() is almost always the wrong tool.
locals() is different. Inside a function, the dictionary it returns is a snapshot of the local namespace, not a live view. Writing to it does not reliably update the actual local variables, because Python optimizes function-local variables into slots that don't go through the dict. Treat locals() inside a function as read-only.
The actual local subtotal is unchanged. The dictionary returned by locals() was modified, but that didn't feed back into the function's real locals. This is one of the few places where Python's "everything is a dictionary" mental model leaks.
Cost: locals() and globals() are O(n) in the number of names in the scope (they build or hand back a dict). For debugging this is fine; for hot loops, don't call them per iteration.
At the module level, things are simpler. locals() and globals() return the same dictionary, because at the module level the "local" scope is the module's namespace.
This is one of those facts that doesn't matter much in day-to-day code but does explain a few odd error messages and tutorials. Inside a function, local and global are different namespaces. At the top of a file, they're the same one.
A small diagram pins the relationship together:
The green box is the module's global namespace, the same dict that globals() returns from anywhere in the file. Each function has its own orange local namespace, which exists only while the function is running. The teal box is the built-in module, always available as the last fallback for any name lookup. Dotted lines show the direction LEGB takes when a name isn't found in the more local scope.
You might have noticed that everything in this lesson is about reading names. We read globals from inside functions, we shadow them with local names, and we hit UnboundLocalError when we assign in a way that confuses the lookup. What we haven't done is rebind a global or enclosing variable from inside a function.
That's not because it's impossible. Python has two keywords designed for exactly that case:
global name tells Python "in this function, the name refers to the module-level binding, even if I assign to it." This is what would fix the UnboundLocalError example if you actually wanted to change the global.nonlocal name does the same thing for enclosing-function scope. It says "in this inner function, this name refers to the local of the enclosing function, even when I assign."Both are full topics on their own, with their own pitfalls and idioms. The takeaway is: if you find yourself wanting to assign to a name from an outer scope inside a function, the answer is one of those keywords, not a clever rearrangement of code.
Closures build on enclosing scope and nonlocal. The classic "function that remembers a value from its enclosing scope" pattern relies on them. The LEGB rule covered in this lesson is the foundation.
10 quizzes