🐍 Python Course · Stage 4 · Lesson 65 of 89 · Concept: Scope
Course Overview →

🟩 These concept pages connect Python to programming in general. Read Beginner tab first.

← Concept: Functions Concept: Classes →
thecodex.expert  ·  The Codex Family of Knowledge
CS Concepts

Scope

The rule that determines which declaration a name refers to — and why the same name can mean different things in different places.

CS Concepts Language-independent ~18 min read Last verified:
Canonical Definition

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.

🟩 Beginner

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.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: Functions
One sentence that captures it

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.

Pythonscope.py
x = "global"

def my_function():
    x = "local"    # new binding, only inside this function
    print(x)       # "local"

my_function()
print(x)           # "global" — unchanged

LEGB 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.

Pythonlegb.py
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.

Pythonclosure.py
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

Continue to Classes →

🔵 Intermediate

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.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: Beginner tab
📎

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.

JavaScriptvar_vs_let.js
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.

Commonly confused
Scope is not the same as lifetime. Scope = where a name is visible in source code. Lifetime = how long the storage persists at runtime. A closure keeps storage alive beyond the function's scope — the name is out of scope but the memory is still live.
JavaScript's 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.
Python has no block scope. A variable assigned inside a for loop is still accessible after the loop. Only def and class create new scopes in Python.
How this connects
Requires first
Enables
Confused with

✅ 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

Continue to Classes →

🔴 Expert

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.

⏱ 15 min 📄 3 sections 🔶 Prerequisite: After Stage 2

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.

Specification reference

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

Continue to Classes →

Sources

1
Python Software Foundation. Python Language Reference, §4.2.2 — Resolution of names. docs.python.org/3/reference/executionmodel.html.
2
Aho, A. V. et al. (2006). Compilers, §2.7. Pearson. — Symbol tables and lexical scope.
3
ECMAScript 2024, §13.3 — Declarations and the Variable Statement. Ecma International.
4
The Rust Reference, §6.3 — Scopes. doc.rust-lang.org/reference/.
Source confidence: High Last verified: Primary source: Python Language Reference §4.2.2