Scope is the region of a program's source code within which a given name binding is visible and can be referenced — determined either by the lexical structure of the source text (lexical scope, used by most modern languages) or by the runtime call chain (dynamic scope), with inner scopes able to shadow names from outer scopes.
Where in your code a name can be used
What you'll learn: Why scope exists (preventing name collisions), Python's LEGB rule, and what shadowing is. After this, NameError will make sense instead of being mysterious.
How to read this tab: Test scope in the REPL: define a variable inside a function, then try to access it outside. The NameError you get is scope working as intended.
Scope is the neighbourhood a variable lives in — inside a function, outside it, or everywhere — and code from one neighbourhood cannot see variables from a different one.
Scope prevents name collisions. Without scope, every function in every library would need globally unique variable names. With scope, every function gets its own namespace. Your function's variable named "total" doesn't conflict with the library's variable named "total" — they're in different scopes. Scope is what makes large programs possible.
Why scope exists
Without scope, every variable would be visible to every piece of code. A variable called count in one function might accidentally conflict with count in another. Scope gives each function a private space for its local variables that nothing outside can touch.
x = "global"
def my_function():
x = "local" # new binding, only inside this function
print(x) # "local"
my_function()
print(x) # "global" — unchangedLEGB is Python's lookup order: Local → Enclosing → Global → Built-in. When Python encounters a name, it searches these four scopes in order, stopping at the first match. This explains: why you can use print() anywhere (Built-in), why global variables are accessible inside functions (Global), why closures work (Enclosing), and why local variables shadow outer ones (Local wins).
Python's LEGB rule
Python looks up names in order: Local, Enclosing, Global, Built-in. First match wins.
x = "global"
def outer():
x = "enclosing"
def inner():
print(x) # finds "enclosing" in E scope
inner()
outer() # prints "enclosing"Shadowing is legal but confusing. If a local variable has the same name as a global, the local "shadows" the global inside that function. The global is unaffected. This is a frequent source of confusion: x = 10; def f(): x = 20; return x; print(x) — the global x is still 10 after calling f(). If you want to modify the global, you need global x inside the function.
Shadowing
Shadowing occurs when an inner scope defines a name that also exists in an outer scope. The inner definition hides the outer one within that scope.
A closure remembers the enclosing scope even after the enclosing function returns. This is how factory functions work: def make_multiplier(n): return lambda x: x * n. The returned lambda "closes over" n — it carries n with it. Every call to make_multiplier() creates a new closure with its own n. Closures are what makes decorators, callbacks, and partial application possible in Python.
Scope and closures
A closure is a function that captures variables from its enclosing scope, even after that scope's function has returned. Closures work because of lexical scope — the function captures the scope at definition time.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2 — count persists!✅ Beginner tab complete — check your understanding
- I can explain Python's LEGB rule: Local → Enclosing → Global → Built-in
- I know that variables defined inside a function are not accessible outside it
- I know what shadowing is — when a local variable has the same name as an outer one
Closures, lexical vs dynamic scope, and block scope
What you'll learn: Closures — functions that capture variables from their enclosing scope. The difference between lexical scope (Python, most languages) and dynamic scope (rare). Block scope in JavaScript (let/const) vs Python's lack of block scope.
How to read this tab: The closure section is directly tied to decorators and callbacks. Read it and then look at a Python decorator — you'll see the closure pattern immediately.
Python uses lexical (static) scope. The scope of a name is determined by where it appears in the source code — not by the call chain at runtime. Most modern languages use lexical scope. Dynamic scope (where the lookup follows the call chain) is rare — Emacs Lisp and some shells use it. Lexical scope is easier to reason about and debug.
Lexical vs. dynamic scope
Lexical scope: names resolve to their enclosing definition in source code — resolved at compile time. Used by Python, JavaScript, Java, C, Rust, Go. Dynamic scope: names resolve to the most recently bound value at runtime along the call chain. Used historically by early Lisps, Emacs Lisp, Bash. Dynamic scope is harder to reason about — the same code produces different results depending on the call history.
Python has no block scope. In most languages (Java, C, JavaScript with let), variables declared inside an if or for have block scope — they disappear when the block ends. In Python, if x = 1 inside an if block, x is accessible after the if. This is intentional but catches developers from other languages off guard.
Block scope vs. function scope
Block scope (C, Java, Rust, JavaScript let/const): variable visible only within { } block. Function scope (JavaScript var): visible throughout entire function, hoisted to top. var hoisting is a source of bugs — let/const replaced it in ES6.
function fn() {
console.log(x); // undefined (var hoisted, not error)
var x = 5;
}
function fn2() {
console.log(y); // ReferenceError: temporal dead zone
let y = 5;
}global and nonlocal are signals of design issues, not normal tools. If you find yourself using global frequently, the code usually needs to be restructured — perhaps into a class that holds the shared state. nonlocal is used in closures when you need to reassign (not just mutate) a variable from the enclosing scope. Reassignment without nonlocal creates a new local variable instead.
Python's global and nonlocal keywords
global x: subsequent references to x in the function refer to module-level x. nonlocal x: refers to nearest enclosing function's x. Without these, assignment always creates a new local binding.
var does not have block scope. A var declared inside an if block is visible throughout the entire function. let and const (ES6) introduced block scope.for loop is still accessible after the loop. Only def and class create new scopes in Python.✅ Intermediate tab complete — check your understanding
- I can explain what a closure is: a function that captures a free variable from its enclosing scope
- I know Python's for loop does NOT create a new scope — the loop variable exists after the loop
- I know Python uses lexical (static) scope, not dynamic scope
Symbol tables, the TDZ, and compile-time name resolution
What you'll learn: Symbol tables and how compilers track names. The Temporal Dead Zone in JavaScript. How Python's global and nonlocal keywords work at the bytecode level.
How to read this tab: Read when studying compilers or when debugging subtle scoping bugs in Python.
Symbol tables and compile-time name resolution
During compilation, the semantic analysis phase builds a symbol table — a stack of hash tables. The compiler pushes a new table on entering a scope and pops on leaving. Name lookup walks the stack innermost-to-outermost. For lexically scoped languages, all name bindings are resolved before the program runs — the compiler can detect undefined-variable errors statically.
The temporal dead zone (TDZ)
JavaScript let/const declarations are hoisted to the top of their block (existence known) but not initialised until the declaration line is reached. Accessing them before that line raises a ReferenceError — the variable is in the temporal dead zone. Specified in ECMAScript 2015 §13.3.2. The TDZ converts silent var undefined reads into explicit errors.
Python LEGB: Language Reference §4.2.2. Aho et al. (2006) §2.7 — Symbol Tables. ECMAScript 2024 §13.3 — Declarations. TDZ: ECMAScript 2015 §13.3.2.
✅ Expert tab complete
- I know that Python decides at compile time whether a name in a function is local or global
- I know the UnboundLocalError: using a name before assignment in the same function that also assigns it
- I understand what the JavaScript Temporal Dead Zone is