Static vs Dynamic Scope

Imagine a function `f` that refers to a variable `x` it never declares itself. Which `x` does it mean? Is it the one visible where `f` was written? Or the one visible where `f` happens to be called? This single question splits every programming language into one of two philosophical camps. Getting it wrong is one of the most confusing bugs a beginner can hit. The program looks correct on the page yet misbehaves at runtime. It depends on who calls whom. The choice of scoping discipline is not a cosmetic detail. It determines whether you can understand code by reading it in isolation. Or whether you must trace the entire call history of a running program. Only then can you know what a name means. Compilers for statically scoped languages can resolve most variable references at compile time. They bake them into fast fixed-offset memory accesses. They also give you clear error messages before the program ever runs. Dynamically scoped languages, in contrast, resolve names against the live call stack. This trades predictability for a flexible form of implicit parameter passing. Some systems actively want this — shells, some macro systems, some UI frameworks. This article builds the concept from first principles. It uses the idea of a free variable and an environment. This is the same machinery introduced when studying Environments, Names, Binding, and Scope. We define static and dynamic scope precisely. We walk through the algorithms a compiler or interpreter uses to resolve free variables under each discipline. We show this with a worked trace where the two disciplines disagree. This reveals exactly how and why the answers diverge. By the end you will be able to predict, for any snippet of pseudocode, what a statically scoped implementation prints. You will also predict what a dynamically scoped one prints. You will understand the implementation techniques that make each discipline efficient. These include lexical addressing, environment chains, and dynamic binding stacks. This sets up later material on The Functional Paradigm, where lexical scope becomes the backbone of closures. It also sets up Parameter Passing Mechanisms, where the scoping rule interacts with how arguments are bound.

Key takeaways
  • Static (lexical) scope: a free variable refers to the binding visible in the enclosing text where the function was defined.
  • Dynamic scope: a free variable refers to the most recent still-active binding on the call stack, determined at call time.
  • Static scope lets you resolve names by reading source code alone; dynamic scope requires knowing the runtime call history.
  • Static scope is implemented with lexical addressing (compile-time depth/offset pairs) or environment chains; dynamic scope is implemented with a binding stack or shallow/deep binding.
  • Static scope is the near-universal default (C, Java, Python, Scheme, ML); dynamic scope survives in shells, some Lisp special variables, and a few configuration/DSL contexts.
  • The two disciplines agree whenever a function has no free variables captured from an outer enclosing scope that differs from the caller's scope — they diverge exactly when call-site environment and definition-site environment differ.
The core insight

A free variable's meaning is fixed by where a function is written under static scope, and by who calls it under dynamic scope — the same program can print two different answers depending solely on which rule the language enforces.

The idea

A variable occurrence inside a function body is either bound — declared as a parameter or local of some enclosing construct — or free, meaning it is not declared locally and must be resolved against some outer context. Scope is the policy that decides, for a free occurrence of a name, which binding of that name it denotes. This decision must be made either once, at the point the code is written (static scope), or repeatedly, at the point the code executes (dynamic scope). Under static scope (also called lexical scope), the binding for a free variable is the one declared in the smallest enclosing block, function, or module that textually contains the reference, searching outward through the nesting of the source code exactly as written — never through the chain of function calls. Because this search only depends on the program text, it can be performed once, by a compiler, before the program ever runs. The result is that a function's meaning is self-contained: you can determine what every name refers to by looking at the definition site and its lexical ancestors, regardless of who calls the function or from where. Under dynamic scope, the binding for a free variable is the most recent binding for that name that is still active on the call stack at the moment the reference is evaluated — i.e., the search walks backward through the chain of callers, not through the nesting of the source text. A function's meaning under dynamic scope depends on the entire history of calls that led to its invocation; the same function body can resolve a free name differently on two different calls, purely because the caller (or the caller's caller) happened to have declared a local variable with that name. This makes dynamic scope a form of implicit, name-based parameter passing — useful for things like ambient configuration, but dangerous for reasoning about correctness, because the meaning of an identifier is not determined by the code you are staring at. Almost every mainstream general-purpose language today — C, Java, Python, JavaScript, Scheme, ML, Rust — is statically scoped. Dynamic scope persists in specific corners: Unix shell variable and function lookup along certain paths, Emacs Lisp's "special variables," LaTeX macro expansion in some respects, and any ad hoc "context object" threaded implicitly through a call chain (thread-local variables and `contextvars`-style mechanisms are a controlled, opt-in form of dynamic scope layered on top of an otherwise lexically scoped language).

How it works

The cleanest way to see the difference is to look at the algorithm each discipline uses to resolve a free variable reference, and to notice that they consult different data structures: static scope consults the lexical nesting of the abstract syntax tree, dynamic scope consults the runtime call stack.

Static scope resolution. At compile time (or, for an interpreter, at closure-creation time), we walk outward through the lexical enclosing scopes of the point where the variable occurs in the source, stopping at the first scope that declares it. ``` // STATIC SCOPE RESOLUTION // Performed using the AST's lexical nesting, independent of any call. function resolveStatic(name, lexicalScopeChain): scope = lexicalScopeChain // innermost enclosing scope first while scope != null: if scope.declares(name): return scope.bindingFor(name) scope = scope.lexicalParent // determined by SOURCE TEXT nesting error "unbound variable: " + name // A closure captures its DEFINITION-TIME lexical scope chain. function makeClosure(functionBody, definitionScope): return Closure(code = functionBody, capturedScope = definitionScope) function callStatic(closure, args): newScope = extend(closure.capturedScope, bind(closure.params, args)) return evaluate(closure.code, newScope) // free vars resolve via capturedScope ``` The crucial line is `closure.capturedScope`: a closure remembers the scope chain that was lexically in effect where it was written, and every future call reuses that same chain to resolve free variables, no matter where the call originates.

Dynamic scope resolution. There is no notion of a captured definition-time scope. Instead, every function call pushes its local bindings onto a single global stack, and a free variable is resolved by searching that stack from the top (most recent call) downward. ``` // DYNAMIC SCOPE RESOLUTION // Performed against the RUNTIME CALL STACK, not the source text. globalBindingStack = [] // stack of activation records, most recent on top function resolveDynamic(name): for frame in globalBindingStack (top to bottom): if frame.declares(name): return frame.bindingFor(name) error "unbound variable: " + name function callDynamic(functionBody, params, args): frame = bind(params, args) push(globalBindingStack, frame) result = evaluate(functionBody) // free vars resolve via CURRENT stack pop(globalBindingStack) return result ``` Notice the symmetry and the key difference: both algorithms perform an outward/downward search through a chain of frames looking for the first declaration of `name`. Static scope's chain is fixed at closure-creation time and reflects source nesting; dynamic scope's chain changes on every call and reflects the current call history.

Two implementation strategies for dynamic scope are worth naming, because they trade off lookup cost against save/restore cost:

  • Deep binding: pass the entire current binding stack (or a reference to it) along wherever a function value travels, so a later call resolves free variables against the stack as it stood at the moment the function value was captured, walked at call time. This is what our pseudocode does implicitly by having one global stack that grows/shrinks with calls.
  • Shallow binding: maintain one mutable "current value" cell per variable name (like a global slot); on entering a scope that rebinds `x`, save the old value, overwrite the cell, and restore the old value on exit. Lookup is then O(1) (just read the cell) instead of a stack search, at the cost of save/restore bookkeeping on every call — this is exactly how Lisp's dynamic `let`/`special variable` binding and Unix shell dynamic scoping are typically implemented.

Lexical addressing, the standard compiler optimization for static scope, deserves a mention because it shows why static scope is fast: since the lexical nesting is fixed at compile time, a compiler can replace every variable reference with a pair `(depth, offset)` — how many enclosing scopes to skip, and which slot in that scope — computed once, ahead of time, so that at runtime resolution is a fixed number of pointer dereferences with no name comparison at all. ```

Figure
Static scope resolves via lexical (source) nesting; dynamic scope resolves via the call stack
Static scope: source-text nesting program scope x = 10 function f (defined here) y = 1 refers to free "x" → x = 10 ✓ function g (defined here, calls f) x = 99 (a LOCAL x) call f() f's free "x" IGNORES g's x = 99 because f was not lexically nested inside g Dynamic scope: call-stack lookup Call stack (top = most recent) frame: f() — local: y = 1 free "x" → search DOWN the stack → frame: g() — local: x = 99 FIRST match found here → x = 99 ✓ frame: main() — local: x = 10 never reached — search stopped above Same program, same call — dynamic scope returns x = 99, static scope returns x = 10. Rule: dynamic search follows CALLERS, static search follows lexical PARENTS.

Complexity & correctness

Time complexity of resolution.

  • Static scope, naive environment chain: each free-variable lookup walks the chain of enclosing scopes, costing `O(d)` where `d` is the lexical nesting depth at that point, and each scope's own lookup (declares/bindingFor) costs `O(1)` if scopes are implemented as hash maps or `O(k)` for a linear scan of `k` locals. Since `d` and `k` are both bounded by the size of the program text — not the runtime call depth — this cost is bounded by a compile-time-known constant for any fixed program.
  • Static scope with lexical addressing: because `d` and the offset are computed once at compile time, a lookup at runtime is exactly one fixed sequence of `depth` pointer hops plus one array index — `O(depth)` pointer chases with no comparisons, and `depth` is a compile-time constant per reference site, effectively `O(1)` amortized per access in generated code (some compilers flatten further into direct register/stack-slot allocation, making it truly O(1)).
  • Dynamic scope, stack search: a lookup walks the call stack from the top until it finds a frame declaring the name, costing `O(c)` where `c` is the current call depth — a runtime quantity that can grow without any bound related to the program's static size (e.g., inside recursion). This is the fundamental asymmetry: static scope's cost is bounded by a static property of the program, dynamic scope's cost is bounded by a dynamic property of the execution.
  • Dynamic scope, shallow binding: lookup is `O(1)` (direct cell read), but every call that shadows a variable pays `O(1)` save-and-restore overhead symmetric with entry/exit, so the total work across a computation is proportional to the number of (call, shadowed-variable) pairs, which is the same asymptotic work as deep binding, merely redistributed from lookup-time to call-time.

Space complexity. Static scope needs space proportional to the nesting structure of the source plus one environment frame per active closure invocation, exactly as much as the live data requires — this is the same frame structure used for ordinary stack-discipline function calls, augmented with saved parent pointers for closures that outlive their creating call. Dynamic scope needs one frame per active call on the global stack (same order as static scope's runtime frames), but no extra parent-pointer chain is needed since the entire call stack itself is the search structure.

Correctness argument — why static scope makes "read the code" a sound proof strategy. Define the lexical scope chain of a program point `p` as the finite sequence of enclosing block/function headers containing `p`, ordered innermost to outermost; this sequence is a syntactic property of the AST and is fixed for a given program text, independent of execution. Static scope resolution is precisely a deterministic function `resolveStatic : (Name, ASTNode) → Binding`, computable by a single traversal of the AST with no reference to any runtime state. Consequently, for a statically scoped language, the meaning of every variable reference is a pure function of the program text alone — this is exactly the property that lets a type checker, a linter, or a human reader determine "unbound variable" errors and shadowing behavior without running the program, and it is what justifies the standard closure-conversion transformation used in compiling The Functional Paradigm's higher-order functions: a closure can be compiled once into `(code pointer, captured environment)` and that pair is valid at every future call site, because the captured environment is exactly, and only, the lexical chain computed at definition time. Dynamic scope resolution is instead a function of program text and full call history: `resolveDynamic : (Name, CallStack) → Binding`. Two calls to syntactically identical code can resolve the same free name to different bindings whenever the call stacks differ, which means no local (AST-only) analysis can, in general, determine what a free variable refers to — in the worst case you must consider every possible caller, transitively, which is why dynamically scoped programs are notoriously hard to reason about modularly: a change to a caller's local variable name can silently change a callee's behavior, an effect sometimes called inadvertent capture. This is not a defect in any particular dynamic-scope implementation; it is a direct, provable consequence of making the resolution function depend on the call stack rather than on the AST.

Honest tradeoff. Dynamic scope is not "wrong" — it is a deliberate design choice when a program wants ambient, caller-supplied context without threading extra parameters explicitly (this is precisely what Unix shell environment variables, Lisp special variables such as `standard-output`, and thread-local/"implicit parameter" mechanisms in modern languages provide, in a controlled and clearly marked form). The tradeoff is: dynamic scope buys implicit, flexible context-passing at the cost of losing the "read the definition, know the meaning" guarantee that static scope provides, and losing compile-time detectability of unbound-variable errors in general (since whether a name is bound can depend on the call path actually taken at runtime).

Worked example

Consider this pseudocode, written to make static and dynamic scope give different answers: ``` x = 10 function f(): return x + 1 function g(): x = 99 return f() print(g()) ``` Trace the resolution of the free variable `x` inside `f` under each discipline.

StepStatic (lexical) scopingDynamic scoping
1. Where is `f` defined?At top level, lexically nested only inside the program scope where `x = 10` lives.(irrelevant — dynamic scope ignores definition site)
2. `g()` is called; `g` binds a local `x = 99`.`g`'s local `x` is invisible to `f`, because `f` is not lexically nested inside `g`.Call stack becomes `[main, g]`; frame for `g` holds `x = 99`.
3. Inside `g`, `f()` is called.`f`'s free `x` resolves via `f`'s captured lexical scope — the top-level scope — regardless of who called `f`.Call stack becomes `[main, g, f]`. `f`'s free `x` search starts at the top of the stack.
4. Resolve `x` inside `f`'s body.Search: `f`'s local scope (no `x`) → top-level scope → found `x = 10`.Search: `f`'s frame (no `x`) → `g`'s frame → found `x = 99` first.
5. Return value of `f()``10 + 1 = 11``99 + 1 = 100`
6. Return value of `g()`, printed11100

The two disciplines diverge at exactly the moment predicted by the theory above: `f`'s lexical parent is the top-level scope (because that is where `f` was written), but `f`'s nearest caller with an `x` binding is `g` (because that is who called it). Static scope follows the dashed line back through source nesting; dynamic scope follows the solid line back through the call stack — and in this example those two lines lead to different bindings of `x`, so the printed output differs: 11 under static scope, 100 under dynamic scope, from the identical program text. A second, shorter case shows the disciplines agreeing, to make clear the divergence is not automatic: if `g` never rebinds `x` at all, both disciplines find `x = 10` on every path, and both print `11` — the difference only surfaces precisely when a caller shadows a name that a callee's free variable also refers to.

Common pitfalls & traps

PitfallWhy it happensFix / mitigation
Assuming a language is "obviously" statically scoped without checking special formsMany statically scoped languages (Common Lisp, Emacs Lisp, Bash) mix in dynamically scoped "special" variables for specific purposesCheck the language spec for keywords like `defvar`/`declare special` or dynamic `export`; treat those names as a distinct, marked exception
Confusing dynamic scope with late binding of method dispatchBoth involve "resolved at runtime," so the vocabulary sounds similarLate binding (virtual dispatch) resolves which code runs based on an object's runtime type; dynamic scope resolves which variable binding a name refers to based on the call stack — orthogonal concepts
Believing closures "copy" variables at creation timePeople assume capturing a variable snapshots its current valueA lexical closure typically captures a reference to the binding (the storage location), so later mutations of that variable are visible through the closure — this is why loop-variable capture bugs happen in some languages
Forgetting that dynamic scope makes recursion resolve to the innermost still-active call's bindings, not the outermostIntuition from static scope (outermost enclosing wins ties) doesn't transferUnder dynamic scope, always search from the top (most recent) of the call stack downward — the newest matching frame wins
Treating "global variable" bugs in static languages as if they were scope-discipline bugsAccidentally reusing a global name feels similar to dynamic-scope captureIt's not the same mechanism: a shared mutable global under static scope is a single, statically determined binding being mutated, whereas dynamic-scope capture changes which binding is even selected

Pioneers & history

The static/dynamic scope distinction crystallized out of the first wave of high-level, block-structured, and list-processing languages, when implementers first had to decide, concretely, how a free identifier inside a procedure should be resolved.

Origins · 1 minJohn McCarthy and how this was discovered
JM
John McCarthy
1927–2011 · Designed LISP (1958) at MIT, whose original implementation used dynamic scope, exposing the problem for the first time in a widely used language

McCarthy's LISP popularized functions as first-class values and recursive procedure definition, but its early interpreters resolved free variables dynamically — through the current call stack — which led directly to the discovery of the "funarg problem": passing a function as an argument or returning it as a result behaves unpredictably if free variables are resolved by the caller's environment rather than the environment where the function was written.

History
1958
— McCarthy's LISP is implemented with dynamic variable scoping, later exposing the "funarg problem."
1960
— ALGOL 60 formalizes block structure with strictly static (lexical) scoping, establishing the now-dominant discipline.
1975
— Scheme (Sussman and Steele) adopts fully lexical scope with first-class closures, resolving the funarg problem cleanly.
1984
— Common Lisp standardizes explicit `defvar`/"special variable" declarations, allowing controlled, opt-in dynamic scope alongside a default of lexical scope.
1980
s–1990s — nearly all mainstream languages (C, Pascal, Ada, ML, Java) settle on static scope as the default, with dynamic-scope-like mechanisms (thread-locals, dynamic-extent bindings) offered only as explicit, marked library features.

The funarg problem is worth naming explicitly because it is the historical fossil record of this entire article's argument: early Lisp implementers discovered, empirically, that returning a function from another function and calling it later gave surprising results under dynamic scope, because the free variables inside the returned function picked up whatever happened to be on the stack at the call site rather than what was in scope at the definition site. The fix — remembering the definition-time environment alongside the code, i.e., the closure — is exactly the `capturedScope` field in the static-scope pseudocode above, and it became one of the most consequential ideas in the history of programming languages.

Where this matters

Compilers and virtual machines. Static scope is what allows a compiler to perform lexical addressing, stack-frame layout, and register allocation entirely at compile time, producing the tight, offset-based variable access used by virtually every production compiler for C-family, ML-family, and JVM/CLR-targeted languages — dynamic scope would force every variable access to become a runtime stack search, which is untenable at the performance levels modern compilers target.

Shells and configuration DSLs. Unix shells resolve many variables (and in some shells, functions) dynamically along the call chain of sourced scripts, which is precisely why sourcing a script can silently change the behavior of functions defined elsewhere — a direct, everyday instance of the inadvertent-capture trap discussed above, and a reason shell scripting is notoriously fragile at scale.

Lisp dialects and "special" variables. Common Lisp and Emacs Lisp expose an explicit, marked dynamic-scope mechanism (`defvar`, "special" variables) precisely because certain use cases — rebinding `standard-output` for the duration of a call, or customizing a text editor's behavior contextually — genuinely want caller-supplied ambient context, and dynamic scope is the natural tool for exactly that need, so long as it is opt-in rather than the silent default.

Thread-local storage and context propagation. Modern systems (web frameworks propagating a request's tracing context, logging frameworks propagating a correlation ID) implement a controlled, structured form of dynamic scope — "the value visible depends on the current execution context" — precisely because passing such values as explicit parameters through every intervening function would be prohibitively invasive; these mechanisms are dynamic scope's legitimate modern descendants.

Closures and the functional paradigm. Every closure-based idiom — callbacks, iterators, currying, memoization wrappers — depends on static scope's guarantee that a function's free variables are pinned to its definition-time environment; this guarantee is the foundation the entire discussion of The Functional Paradigm builds on, and it interacts closely with Parameter Passing Mechanisms whenever a language must decide whether captured variables are copied by value or shared by reference.

Ask the tutor: Can you show how the same funarg-problem example would be compiled step by step into lexical-addressed bytecode under static scope?

Is dynamic scope ever actually a good design choice?

Yes, in narrow, deliberate cases. Dynamic scope is useful when you want a function to pick up ambient context supplied by its caller without threading that context through every intervening parameter list — for example, temporarily rebinding an output stream, a locale, or a logging context for the duration of a call. Common Lisp's "special variables," Emacs Lisp's dynamically-scoped bindings, and modern "context variable" or thread-local mechanisms all exploit this deliberately, usually with explicit syntax marking the variable as dynamically scoped so readers are warned. The danger is using dynamic scope as the unmarked default for all variables, which is what makes shells and old Lisp fragile — the fix historically was to keep static scope as the default and make dynamic scope opt-in and visible.

How does static scope actually get implemented efficiently at runtime?

Two common strategies exist. The first, environment chains, represents each scope as a small record (a frame) with a pointer to its lexically enclosing frame; resolving a variable walks this chain at runtime, which is simple but costs time proportional to nesting depth per lookup. The second, lexical addressing, is used by production compilers: since the nesting structure is fixed by the source text, the compiler computes, once, a `(depth, offset)` pair for every variable reference, so a lookup at runtime becomes a fixed number of pointer hops plus a direct array index, with no name comparisons at all. Both approaches rely on the same underlying fact — the chain of enclosing scopes is determined entirely by where code is written, never by how it is called.

What exactly is a closure, and how does it relate to this topic?

A closure is a function value paired with the environment that was lexically in effect at the moment the function was created — informally, "code plus its captured scope." Closures only make sense as a clean, well-defined concept under static scope: because the captured environment is exactly the function's lexical ancestors, calling the closure later, from anywhere, reuses that same fixed environment to resolve free variables. Under dynamic scope there is nothing stable to "capture," since free variables are re-resolved against whatever the call stack looks like at each call — this is exactly the historical funarg problem, and it is why closures, as a reliable abstraction, effectively require lexical scope to work correctly.

Does dynamic scope mean variables cannot be typed or checked at compile time?

Not absolutely, but it makes static analysis dramatically harder. Under static scope, a compiler can determine every variable's binding — and therefore its type — by a single traversal of the program text, catching unbound-variable errors before execution. Under dynamic scope, whether a given free variable reference is even bound at all can depend on which call path was actually taken at runtime, so a sound compile-time checker would, in the worst case, need to consider every possible caller transitively, which is generally infeasible for arbitrary programs. This is a major practical reason dynamic scope is rare in languages that emphasize strong static type systems.

How do I tell, just by reading code, whether a language uses static or dynamic scope?

The decisive test is the funarg-style experiment used in the worked example: define a function that references a free variable, define a second function that locally shadows that same variable name and then calls the first function, and observe which value is used. If the result matches the variable's binding at the first function's definition site, the language is statically scoped; if it matches the caller's local binding, it is dynamically scoped. In practice, checking the language reference for terms like "lexical scope," "closures capture their environment," or conversely "special variables"/"dynamic extent" tells you immediately, since almost all modern general-purpose languages document this explicitly.

Why did nearly every modern language converge on static scope as the default?

Because static scope makes programs locally reasoned about: the meaning of any variable reference is a function of the surrounding code alone, not of the entire runtime call history, which is essential once programs are built from independently-written modules, libraries, and functions passed around as values. This locality enables compile-time error detection, safe and efficient closures, predictable behavior under refactoring, and fast compiled code via lexical addressing. Dynamic scope's flexibility — implicit ambient context — turned out to be needed only in specific, narrow situations, which is why it survives as an explicit, opt-in feature (special variables, thread-locals) rather than as the default resolution rule.

What is the relationship between scope discipline and parameter passing?

They address related but distinct questions: scope discipline decides which binding a free (non-parameter) variable refers to, while parameter passing mechanisms decide how arguments become bound to parameters (by value, by reference, by name, and so on) — a topic developed further under Parameter Passing Mechanisms. The two interact because dynamic scope can be viewed as an implicit, unnamed channel of "extra parameters" supplied by the call stack, whereas under static scope, explicit parameters plus lexical closures are the only mechanisms by which a callee's behavior can depend on its caller.

Before this

Where this leads