Lexical Scope and Free Variables
Overview
Lexical scope means that a function's access to variables is determined by where the function is DEFINED in the source code, not where it is called. def outer(): rate = 1.2 def inner(): return rate # 'rate' is a free variable, not a local one return inner fn = outer() print(fn()) # 1.2 -- inner() still sees 'rate' after outer() has returned When an inner function references a variable from an outer function, that variable is called a 'free variable'. Even after the outer function finishes executing, the inner function retains access to it. Think of lexical scope like looking through a one-way mirror: the inner function can see out to the environment where it was born, but the outside cannot see in.
Lexical scope is the foundation for closures and decorators. It allows functions to carry state without relying on global variables or classes.
Where used: React-like state management, FastAPI dependency injection, Decorators
Why learn this
- Understanding closures and decorators
- Avoiding late-binding bugs in loops
- Writing clean, class-less stateful functions
Code walkthrough
def outer():
x = 10
def inner():
print(x)
return inner
func = outer()
func()
Focus: print(x)
Aha moment
def create_greeters():
name = 'Alice'
def greeter():
print(f'Hello, {name}')
name = 'Bob'
return greeter
fn = create_greeters()
fn()
Prediction: What does fn() print?
Common guess: Hello, Alice
Lexical scope binds the inner function to the variable itself, not the value it had at the exact moment the function was defined. When `fn()` runs, it looks up the current value of `name`, which is now 'Bob'.
Common mistakes
- Modifying a free variable without nonlocal: If you try to reassign a free variable inside an inner function, Python assumes you are creating a new local variable instead. You must use the `nonlocal` keyword to explicitly tell Python to modify the free variable.
Glossary
- free variable
- A variable that is used inside a nested function but was defined in the surrounding, outer function, e.g. `rate` in `def discount(): return price * rate` where `rate` is defined outside.
- local variable
- A variable that is created inside a function and can only be seen or used within that specific function, e.g. `total = 0` defined inside `def calculate()`.
Recall questions
- What determines a function's access to variables under lexical scoping?
- What is a 'free variable'?
Understanding checks
What will this code output?
DEBUG: System started
Lexical scope dictates that the inner function accesses the variable itself, not a snapshot of its value at the time the function was defined. When `logger()` is finally called, `prefix` has been updated to 'DEBUG:'.
A junior developer believes that inner functions can freely reassign variables from an outer function just by using an equal sign (e.g., `count = 1`). Why is this incorrect?
Without the `nonlocal` keyword, assigning to a variable with the same name inside an inner function creates a new local variable in that scope instead of modifying the outer free variable.
Python assumes variables assigned inside a function are local to that function unless explicitly declared otherwise. If you want to rebind an outer variable, you must declare it as `nonlocal`.
Practice tasks
Lexical Scope and Free Variables
Currently, `multiply` relies on a global variable `factor`. Modify the code to define a function `make_multiplier(factor)` that returns an inner `multiply` function, so that `factor` is a free variable enclosed by lexical scope.
Challenge
Stateful Counter
Create a function `make_counter` that takes a `start` value. It should return an inner function that, when called without arguments, increments the `start` value by 1 and returns the new value. You will need to modify a free variable.
Continue learning
Previous: LEGB rule: variable scope in Python
Previous: Defining functions and return values
Previous: First-class functions: passing and returning functions
Next: Closures
Next: Closure Mechanics