Understanding Closures and Lexical Scope in Python
Course: Python Training
Topic: Understanding Closures and Lexical Scope
Course Link: Python Training Course at JustAcademy
Demo Link: Register for a Course Demo
JustAcademy Homepage: Visit JustAcademy
1. Introduction to Closures and Lexical Scope
Closures and lexical scope are important concepts in Python that help developers understand how nested functions access variables. These concepts are especially useful when creating reusable functions, decorators, callbacks, function factories, and configurable operations.
Lexical scope determines where a variable can be accessed based on the structure of the source code. Python determines the enclosing scopes of a function from where the function is defined, rather than where it is called.
A closure is a function that retains access to variables from its enclosing function scope, even after that enclosing function has finished executing.
Closures allow an inner function to remember information from its creation environment without requiring that information to be passed as an argument every time.
2. Learning Objectives
- Understand the meaning of scope and lexical scope.
- Identify local, enclosing, global, and built-in scopes.
- Understand nested functions and variable lookup.
- Explain how closures retain access to enclosing variables.
- Create closures using nested functions.
- Understand the difference between
global and nonlocal.
- Use closures to create configurable functions and counters.
- Understand closures in decorators and callback functions.
- Recognize common closure-related errors.
- Apply closures in practical Python projects.
3. Understanding Variable Scope
Scope refers to the region of a program in which a variable or name can be accessed directly. Python uses a scope lookup system commonly summarized as LEGB.
The LEGB Rule
| Scope |
Meaning |
Description |
| Local |
L |
Names defined inside the current function. |
| Enclosing |
E |
Names in enclosing function scopes surrounding the current function. |
| Global |
G |
Names defined at the module level. |
| Built-in |
B |
Names supplied by Python's built-in namespace, such as len() and print(). |
When Python evaluates a name reference in a function, it searches the applicable namespaces according to the LEGB rules. A local binding takes precedence over a binding with the same name in an enclosing scope.
Example: Local Scope
def show_message():
message = "Hello from the local scope"
print(message)
show_message()
Output:
Hello from the local scope
The variable message is defined inside show_message(). It can be accessed directly within that function but is not automatically available as a global variable.
Example: Global Scope
course_name = "Python"
def display_course():
print(course_name)
display_course()
Output:
Python
The function can read course_name because the name is defined in the module's global namespace.
4. What Is Lexical Scope?
Lexical scope, also called static scope, means that the enclosing scope of a function is determined by the location where the function is defined in the source code. The function's access to enclosing variables does not change simply because it is called from another function.
Example: Lexical Scope in Python
def outer_function():
language = "Python"
def inner_function():
print(language)
inner_function()
outer_function()
Output:
Python
The inner function can access language because that name belongs to the enclosing function's scope.
Important Characteristics of Lexical Scope
- Scope relationships are determined by the source-code structure.
- Nested functions can access names from enclosing function scopes.
- Local variables can shadow names defined in outer scopes.
- Calling a function from a different location does not change its lexical enclosing scope.
- Python closures can preserve access to enclosing variables after the outer function returns.
5. Understanding Nested Functions
A nested function is a function defined inside another function. The outer function can create local variables and define inner functions that use those variables.
def outer_function():
def inner_function():
print("This is the inner function")
inner_function()
outer_function()
Output:
This is the inner function
In this example, inner_function() is defined inside outer_function(). It is called from within the outer function.
Returning an Inner Function
An outer function can return an inner function as a value. This is one of the most important building blocks for creating closures.
def outer_function():
def inner_function():
return "Inner function returned"
return inner_function
result = outer_function()
print(result())
Output:
Inner function returned
The statement return inner_function returns the function object itself. The parentheses in result() then call the returned function.
6. What Is a Closure?
A closure is a function that remembers access to variables from its enclosing function scope. A common closure pattern has three parts:
- An outer function defines one or more variables.
- An inner function refers to one or more of those variables.
- The outer function returns the inner function, which can continue using the captured variables later.
Basic Closure Example
def outer_function():
message = "Hello from the closure"
def inner_function():
print(message)
return inner_function
greeting = outer_function()
greeting()
Output:
Hello from the closure
After outer_function() returns, the local name message is no longer directly accessible from the caller. However, the returned inner function retains access to the enclosing binding it references.
Why Is This a Closure?
message is defined in the outer function.
inner_function() references message.
- The outer function returns the inner function.
- The returned function can still use
message when called later.
7. How Closures Retain Variables
Python functions can retain references to variables from enclosing function scopes. The captured variable is not necessarily a frozen copy of its value; the closure retains access to the enclosing binding.
def create_greeting(name):
greeting_text = f"Welcome, {name}!"
def greet():
return greeting_text
return greet
greet_student = create_greeting("Asha")
print(greet_student())
Output:
Welcome, Asha!
The variable greeting_text belongs to the enclosing function. The returned greet function can access it even after create_greeting() has finished.
Inspecting a Closure
Python functions may expose captured variables through the __closure__ attribute. Each cell in the closure can be inspected through its cell_contents attribute.
def make_message():
message = "Closure data"
def display():
return message
return display
function = make_message()
print(function())
print(function.__closure__[0].cell_contents)
Output:
Closure data
Closure data
Note: __closure__ is mainly useful for learning, debugging, and introspection. Normal application code generally does not need to inspect closure cells directly.
8. Lexical Scope Versus Closure
Lexical scope and closures are related, but they are not the same concept.
| Feature |
Lexical Scope |
Closure |
| Meaning |
Rules that determine where names are accessible. |
A function retaining access to enclosing variables. |
| Main purpose |
Defines name visibility and lookup. |
Preserves access to selected enclosing bindings. |
| Requires nested functions? |
Not necessarily. |
Commonly involves a nested function. |
| Can outlive the outer function call? |
Scope rules alone do not imply this. |
Yes, a returned closure can continue using captured variables. |
| Typical use |
Understanding variable lookup. |
Function factories, callbacks, and decorators. |
Lexical scope describes the rules for finding names. A closure is a practical function behavior that uses those rules to retain access to enclosing variables.
9. The nonlocal Keyword
The nonlocal keyword allows an inner function to rebind a variable that already exists in an enclosing function scope. Without nonlocal, assigning to a name inside a function generally makes that name local to the current function.
Example Without nonlocal
def outer_function():
count = 10
def inner_function():
count = 20
print("Inner count:", count)
inner_function()
print("Outer count:", count)
outer_function()
Output:
Inner count: 20
Outer count: 10
The assignment count = 20 creates a local variable in inner_function(). It does not rebind the outer function's count.
Example With nonlocal
def outer_function():
count = 10
def inner_function():
nonlocal count
count = 20
print("Inner count:", count)
inner_function()
print("Outer count:", count)
outer_function()
Output:
Inner count: 20
Outer count: 20
The nonlocal declaration tells Python to use the nearest existing binding in an enclosing function scope. The outer variable is therefore updated.
Rules for nonlocal
- The name must already exist in an enclosing function scope.
- It cannot be used to declare a module-level global variable.
- If no suitable enclosing binding exists, Python raises a
SyntaxError.
- The declaration must appear before the name is used or assigned in the relevant function body.
10. Difference Between global and nonlocal
| Keyword |
Refers To |
Typical Use |
global |
The module-level global namespace |
Rebinding a module-level variable from inside a function |
nonlocal |
A variable in the nearest enclosing function scope |
Updating a variable captured by a nested function |
Example: global
counter = 0
def update_counter():
global counter
counter += 1
update_counter()
update_counter()
print(counter)
Output:
2
Example: nonlocal
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter())
print(counter())
print(counter())
Output:
1
2
3
The closure retains the enclosing count binding, and nonlocal allows the inner function to update it without using global state.
11. Creating Function Factories with Closures
A function factory is a function that creates and returns another function. Closures allow the returned function to retain configuration values from the factory call.
Example: Multiplier Factory
def make_multiplier(factor):
def multiply(number):
return number * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
quadruple = make_multiplier(4)
print(double(5))
print(triple(5))
print(quadruple(5))
Output:
10
15
20
Each call to make_multiplier() creates a function with its own enclosing binding for factor.
Benefits of Function Factories
- They allow reusable behavior to be configured once.
- They reduce repeated code.
- They can encapsulate configuration without global variables.
- They make it easy to create specialized functions from a general implementation.
12. Closures for Maintaining State
Closures can maintain state between function calls. This is useful when a function needs to remember a counter, running total, or configuration value.
Example: Counter
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = create_counter()
print(counter())
print(counter())
print(counter())
Output:
1
2
3
The enclosing variable count remains available through the closure. Each call to counter() updates the same captured binding.
Creating Independent Counters
first_counter = create_counter()
second_counter = create_counter()
print(first_counter())
print(first_counter())
print(second_counter())
print(first_counter())
Output:
1
2
1
3
Each call to create_counter() creates a separate enclosing scope, so each returned closure maintains its own counter.
13. Closures and Decorators
A decorator is a function that takes another function and returns a replacement or wrapper function. Decorators often use closures to retain a reference to the original function and any additional configuration.
Example: Simple Logging Decorator
def log_calls(function):
def wrapper():
print("Function is starting")
result = function()
print("Function has finished")
return result
return wrapper
@log_calls
def greet():
print("Hello, Python!")
greet()
Output:
Function is starting
Hello, Python!
Function has finished
The inner function wrapper() retains access to function, which refers to the decorated function. This is an example of a closure being used to implement a decorator.
Preserving Function Metadata
In production code, use functools.wraps to preserve the original function's name, documentation, and other metadata.
from functools import wraps
def log_calls(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Calling {function.__name__}")
return function(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
print(add(3, 4))
Output:
Calling add
7
The wrapper accepts positional and keyword arguments, allowing it to decorate a wider variety of functions.
14. Closures in Callback Functions
A callback is a function passed to another part of a program so it can be called later. Closures allow callbacks to retain information associated with the operation that created them.
Example: Creating a Configured Callback
def make_greeting_callback(greeting):
def callback(name):
return f"{greeting}, {name}!"
return callback
say_hello = make_greeting_callback("Hello")
say_welcome = make_greeting_callback("Welcome")
print(say_hello("Asha"))
print(say_welcome("Rohan"))
Output:
Hello, Asha!
Welcome, Rohan!
Each callback remembers its own greeting. This pattern can be useful for event handlers, configurable operations, and application callbacks.
15. Late Binding in Closures
Late binding is a common source of confusion when closures are created inside loops. A nested function usually looks up the captured variable when it is called, rather than automatically saving the current value of that variable at the moment the function is created.
Example: The Common Loop Problem
functions = []
for number in range(3):
functions.append(lambda: number)
print([function() for function in functions])
Output:
[2, 2, 2]
All three lambda functions refer to the same loop variable number. After the loop finishes, its value is 2, so each function returns 2.
Solution 1: Capture the Value with a Default Argument
functions = []
for number in range(3):
functions.append(lambda number=number: number)
print([function() for function in functions])
Output:
[0, 1, 2]
The default argument is evaluated when each lambda function is created, so each function receives its own saved default value.
Solution 2: Use a Function Factory
def make_function(value):
def inner():
return value
return inner
functions = [make_function(number) for number in range(3)]
print([function() for function in functions])
Output:
[0, 1, 2]
The factory creates a separate enclosing scope for each value. Each closure therefore retains access to its own value binding.
16. Common Closure Errors and Troubleshooting
| Problem |
Cause |
Solution |
| Inner function cannot access a local variable from an unrelated function |
The variable is outside the function's lexical scope |
Pass the value as an argument or create the nested function in the appropriate scope |
| Outer variable does not change after assignment in the inner function |
The assignment creates a local binding |
Use nonlocal when rebinding an enclosing function variable |
SyntaxError with nonlocal |
No suitable enclosing function binding exists |
Ensure the variable is defined in an enclosing function scope |
| All callbacks from a loop return the same value |
Late binding of the loop variable |
Use a default argument or function factory |
| Counter resets unexpectedly |
A new closure is created on each call to the factory |
Create the closure once and reuse the returned function |
| Decorator rejects arguments |
The wrapper does not accept the original function's arguments |
Use *args and **kwargs where appropriate |
17. Advantages of Closures
- Encapsulation: Closures can keep supporting variables inside an enclosing scope.
- State retention: They can maintain values between calls without module-level globals.
- Reusability: Function factories can generate multiple configured functions.
- Flexibility: Closures support decorators, callbacks, and configurable operations.
- Reduced global state: They can limit unnecessary dependencies on global variables.
- Readable configuration: A general operation can be specialized by an outer function.
18. Limitations of Closures
- Nested functions can make code harder to follow if used excessively.
- Captured mutable objects can still be modified, which may cause unexpected behavior.
- Late binding can create bugs when closures are generated inside loops.
- Stateful closures may require careful testing when used by multiple parts of an application.
- Closures can retain references to objects longer than expected, which may affect memory usage.
- Classes may be a clearer option when an operation requires several pieces of state and many related methods.
19. Best Practices for Closures and Lexical Scope
- Use descriptive names for outer functions, inner functions, and captured variables.
- Keep closures small and focused on one responsibility.
- Use
nonlocal explicitly when rebinding an enclosing function variable.
- Avoid unnecessary global variables when a closure or explicit argument is more appropriate.
- Be aware of late binding when creating functions inside loops.
- Use
functools.wraps when writing decorators.
- Test multiple calls to stateful closures to verify that their state behaves as expected.
- Use classes when the state and behavior become too complex for a simple closure.
- Avoid retaining large objects in closures longer than necessary.
- Prefer clear code over complicated nested function structures.
20. Mini Project: Configurable Discount Calculator
Project objective: Create a discount calculator using closures. Each calculator should retain a configured discount rate and calculate discounted prices without using a global variable.
Requirements
- Create a function called
make_discount_calculator().
- Accept a discount percentage as an argument.
- Return an inner function that calculates the final price.
- Create separate calculators for different discount rates.
- Display the calculated prices.
Solution
def make_discount_calculator(discount_rate):
def calculate_discounted_price(price):
discount = price * discount_rate
return price - discount
return calculate_discounted_price
ten_percent_discount = make_discount_calculator(0.10)
twenty_percent_discount = make_discount_calculator(0.20)
print(ten_percent_discount(1000))
print(twenty_percent_discount(1000))
print(ten_percent_discount(2500))
Output:
900.0
800.0
2250.0
Explanation: The outer function receives a discount rate and returns an inner function. Each returned function retains its own discount rate, making it possible to create different calculators from the same implementation.
Possible Improvements
- Validate that the discount rate is between 0 and 1.
- Reject negative prices if the business rules do not permit them.
- Round the result according to the application's currency rules.
- Write unit tests for zero discount, full discount, and invalid input.
21. Practice Exercises
- Create an outer function that stores a user's name and returns a function that displays a personalized message.
- Write a closure that converts temperatures using a configurable offset.
- Create a counter closure that supports both incrementing and reading the current count.
- Build a function factory that creates multipliers for 2, 5, and 10.
- Write a decorator that measures and prints how long a function takes to run.
- Fix a late-binding issue in a list of lambda functions created inside a loop.
- Write two closures with independent state and demonstrate that they do not share their counters.
- Create a closure-based tax calculator that retains a configured tax rate.
- Compare a closure-based counter with a class-based counter and identify when each design is appropriate.
22. Interview Questions and Answers
Q1. What is lexical scope in Python?
Lexical scope means that the enclosing scopes available to a function are determined by where the function is defined in the source code.
Q2. What is a closure?
A closure is a function that retains access to variables from an enclosing function scope, even after the outer function has returned.
Q3. How does Python resolve variable names?
Python commonly follows the LEGB lookup rule: Local, Enclosing, Global, and Built-in.
Q4. What is the difference between a nested function and a closure?
A nested function is defined inside another function. When an inner function retains access to variables from its enclosing scope, it exhibits closure behavior.
Q5. Why are closures useful?
Closures are useful for encapsulating configuration, maintaining state, creating function factories, implementing decorators, and building callbacks.
Q6. What does the nonlocal keyword do?
nonlocal allows a nested function to rebind a name defined in an enclosing function scope.
Q7. What is the difference between global and nonlocal?
global refers to the module-level namespace, while nonlocal refers to a binding in the nearest enclosing function scope.
Q8. Can a closure access variables after the outer function returns?
Yes. The closure retains access to the enclosing bindings it references, allowing it to use them after the outer function call has completed.
Q9. What is late binding in closures?
Late binding occurs when a closure looks up a captured variable when the function is called, rather than saving a separate value when the function is created. It commonly causes surprises in loops.
Q10. How can late binding be avoided?
Use a default argument to capture a value when creating a function, or use a function factory that creates a separate enclosing scope for each value.
Q11. How are closures used in decorators?
A decorator's wrapper function can retain access to the original function and use that reference when the wrapper is called.
Q12. Are closures always better than classes?
No. Closures work well for simple encapsulated behavior and configuration. Classes are often clearer when an application requires several state variables and related methods.
23. Quick Revision Checklist
- Scope determines where a name can be accessed.
- Python uses the LEGB rule for ordinary name lookup.
- Lexical scope is determined by the function's definition context.
- Nested functions can access variables from enclosing function scopes.
- Closures retain access to referenced enclosing variables.
nonlocal rebinds an enclosing function variable.
global refers to the module-level namespace.
- Function factories use closures to create configured functions.
- Closures can preserve state between calls.
- Decorators and callbacks commonly use closure behavior.
- Late binding can cause unexpected results in loops.
- Clear, focused closures are easier to test and maintain.
24. Conclusion
Closures and lexical scope are fundamental concepts for understanding how Python manages variable visibility and nested functions. Lexical scope determines where names are resolved, while closures allow functions to retain access to selected enclosing bindings after the outer function returns.
These concepts are especially valuable for building function factories, counters, decorators, callback functions, and configurable operations. Understanding the LEGB rule, the nonlocal keyword, and late binding helps developers avoid common errors and write more maintainable Python code.
Continue learning with the Python Training Course at JustAcademy, register for a course demo, or visit the JustAcademy homepage.
25. Additional Learning Resources