Python Closures: Coming from JavaScript
Source: Dev.to
Learning with Books vs. Videos
“Books – Gain depth of knowledge on a topic
Videos – Quickly ramp up to use a specific technology”
I still reach for a book whenever I want to learn something new. While today’s documentation, Udemy courses, and other video resources are invaluable, there’s something about a piece of text (digital or hard‑bound) that feels more permanent.
One of my goals for this year is to up my Python skills so I can build AI/ML solutions more comfortably. A key part of that journey has been understanding closures in Python.
Most of the material below comes from Fluent Python (2nd Ed.) and Python Cookbook (3rd Ed.) – highly recommended for practical, engaging Python guidance.
Why Closures Matter
- Decorators – Python decorators are built on closures (more on this later).
- Callbacks – Like JavaScript callbacks, closures let you “remember” state.
- Factory functions – Create specialized functions on the fly (e.g., a multiplier).
- Data encapsulation – Hide internal state without using classes.
If you’ve ever written @app.route() in Flask, you’ve already used a closure—even if you didn’t realize it.
Closure definition
A nested function that remembers and accesses variables from its enclosing (outer) function’s scope, even after the outer function has finished executing, allowing the inner function to maintain state.
Basic Example: Multiplier
Python implementation (mult_closure.py)
def make_multiplier_of(n):
"""Outer function that takes a factor `n` and returns a closure."""
def multiplier(x):
"""Inner function (closure) that uses the remembered factor `n`."""
return x * n
return multiplier
# Create two closures, each remembering a different `n` value
doubler = make_multiplier_of(2) # remembers n = 2
tripler = make_multiplier_of(3) # remembers n = 3
# Results – the scalar is remembered, so we only pass the value to be multiplied
print(f"8 times 2 is {doubler(8)}")
print(f"4 times 3 is {tripler(4)}")
Output
8 times 2 is 16
4 times 3 is 12
Explanation: The outer function returns the nested multiplier function, which retains the value of n.
Visualizing the call styles
# Saved to a variable
doubler = make_multiplier_of(2)
doubler(4)
# Executed in‑place
make_multiplier_of(2)(4)
The Same Idea in JavaScript
JavaScript implementation (mult_closure.js)
function makeMultiplierOf(n) {
// Outer function that takes a factor `n` and returns a closure
function multiplier(x) {
// Inner function (closure) that uses the remembered factor `n`
return x * n;
}
return multiplier;
}
// Instances of our new closure
const doubler = makeMultiplierOf(2);
const tripler = makeMultiplierOf(3);
// Results – the closure remembers the initial value it was passed
console.log(`8 times 2 is ${doubler(8)}`);
console.log(`4 times 3 is ${tripler(4)}`);
Maintaining State Across Calls
Closures can hold mutable data that persists between invocations. Below is a logger that stores messages in a list kept inside the outer function’s scope.
Python logger closure (logger_closure.py)
def create_logger(source):
"""Outer function that holds a list of logs for a given `source`."""
logs = []
def log_message(message=None):
"""
Inner function that appends a message (if provided) and always returns
the current list of logs.
"""
if message:
logs.append({"source": source, "message": message})
return logs
return log_message
# Two independent loggers
error_log = create_logger("error")
info_log = create_logger("info")
# Log some messages
info_log("Hello world")
error_log("File Not Found")
# Retrieve logs
print(error_log("Zero Division"))
print(info_log())
Output
[{'source': 'error', 'message': 'File Not Found'},
{'source': 'error', 'message': 'Zero Division'}]
[{'source': 'info', 'message': 'Hello world'}]
Key point: Each call to create_logger creates its own logs list. Even though error_log and info_log share the same closure structure, they maintain separate state.
Python’s Variable Scoping vs. JavaScript
- JavaScript (pre‑ES6) relied on
var, which required careful handling of function scope. Modern JS usesletandconstto create block‑scoped variables. - Python has no direct equivalent to
let/const. Instead, it follows the LEGB rule (Local → Enclosing → Global → Built‑in) for name resolution. Understanding this rule is essential when working with closures.
TL;DR
- Closures let a nested function remember values from its enclosing scope.
- They’re the backbone of decorators, callbacks, factory functions, and simple stateful objects.
- Python’s lexical scoping (LEGB) makes closures straightforward once you grasp the rule.
Happy coding! 🚀
Example 4 – legb_overview.py: Visual representation of the LEGB rule
"""Global scope"""
x = "global"
def outer():
"""Enclosing scope"""
y = "enclosing"
def inner():
"""Local scope"""
z = "local" # Local scope
# Python searches: Local → Enclosing → Global → Built‑in
print(z) # Found in Local
print(y) # Not in Local, found in Enclosing
print(x) # Not in Local/Enclosing, found in Global
print(len) # Not in Local/Enclosing/Global, found in Built‑in
inner()
outer()
Output
local
enclosing
global
How the LEGB search works
| Scope | Description |
|---|---|
| Local | The interpreter first checks the local namespace of the current function. |
| Enclosing | If the function is nested, Python looks one level up to the enclosing function’s namespace (used heavily with closures). |
| Global | Next, it checks the module‑level namespace (variables defined at the top of the file or imported). |
| Built‑in | Finally, it checks Python’s built‑in namespace (e.g., len, print). |
Important: Python stops searching as soon as it finds a matching name. It does not continue to outer scopes once a match is found.
The nonlocal Keyword
Example 5 – make_avg_err.py: Closure without nonlocal
"""Before"""
def make_avg():
count = 0
total = 0
def inner(new_val):
count += 1 # `nonlocal` is required when you need to **rebind** a variable from an enclosing (but not global) scope. It works for immutable objects like `int`, `float`, `str`, etc.
For mutable objects (e.g., a list), you can modify the object’s contents without
nonlocalbecause you’re not rebinding the name itself (my_list.append(item)).
A quick note for JavaScript developers
Coming from JavaScript, it’s tempting to apply the same patterns directly in Python. However, Python’s scoping rules—especially around closures and the nonlocal keyword—behave differently. Understanding these nuances helps avoid subtle bugs and makes your code more “Pythonic.”
What’s next?
I’m currently building an AST File Parser to explore many of Python’s “small things” that contribute to its efficiency. My goal is to finish the project by the end of the month, after which I’ll dive into other Python topics such as:
- Generators
- Decorators
- Special (magic) methods
Feel free to reach out if you’d like to discuss any of these concepts!