Every time I want to learn something new I always end up doing it the old school way: picking up a book. Sure, in today’s age most of the up-to-date information can be found in a mix of documentation and Udemy courses, but there’s something about picking up a piece of text (digital or hardbound) that makes me think that the knowledge there is meant to last much longer than what’s shown in a video course. I’ve always seen it as:
- Books – Gain depth of knowledge on a topic
- Videos – Quickly ramp up to use a specific technology
That being said, one of my goals for this year is to up my Python skills so that I can be more prepared to build out items for AI/ML use cases. Part of this learning process has been getting to understand Closures in Python. At first I just saw these as a function with a hidden variable (which is technically true), but after doing some digging into what they do and how their scopes are defined I got to learn a bit more about the Python language.
A quick heads up, most of this knowledge was gained from reading Fluent Python, 2nd Ed and Python Cookbook, 3rd Ed. If you haven’t read them already I highly recommend as they provide an in-depth explanation of Python best practices while not being too boring like a text book
Why Closures Matter
Before we dive under the hood, here’s a quick rundown on why closures are an important part of Python to understand early on:
- Decorators – Python decorators are built on closures (more on this in my next post)
- Callbacks – Similar to JavaScript callbacks, closures let you “remember” state
- Factory functions – Create specialized functions on the fly (like our multiplier example)
- Data encapsulation – Hide internal state without classes
If you’ve used decorators like @app.route() in Flask, you’ve used closures—even if you didn’t realize it.
What is a Closure?
Closure: 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
Looking at the definition above, you can see where I was half correct with my initial assumption. Broken down in code, we can take a look at a function like. Using blueprint code, we can start defining our closure like so:
Basic Example: Multiplier
Example 1: mult_closure.py: A closure to make a multiplier out of any function
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. Note that we no longer need to pass a scalar as the closure will remember the initial value it was passed"""
print(f"8 times 2 is {doubler(8)}")
print(f"4 times 3 is {tripler(4)}")
-----
8 times 2 is 16
4 times 3 is 12
As we can see above, we’ve created a new closure that is able to create a multiplier dynamically by saving one of the scalars to memory. This allows developers to only pass through the number they want to see multiplied by instead of always passing in the scalar
Behind the scenes, we’re returning the nested function (the closure) and allowing it to be run right after the multiplier creator is called. Another way to visualize this is side by side:
"""Saved to a variable"""
doubler = make_multiplier_of(2)
doubler(4)
"""Executed in-place"""
make_multiplier_of(2)(4)
JavaScript Equivalent
Coming from JS I was curious to see if there’s an equivalent structure to this. Turns out there is!
Example 2. mult_closure.js: The same multiplier closure from above but in 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)}`);
A Real-Life Implementation
I mentioned before that one of the things a closure can do is maintain state across calls. This can be better visualized if we built a series of log lists:
Example 3. logger_closure.py: Saving log messages to lists in the outer function
def create_logger(source):
"""Outer function now contains a list of logs"""
logs = []
def log_message(message=None):
"""
Inner function, holds the logic that will be run during the call to the outer function.
We'll always return the logs, and only append when a message is provided
"""
if message:
logs.append({"source": source, "message": message})
return logs
return log_message
error_log = create_logger("error")
info_log = create_logger("info")
info_log("Hello world")
error_log("File Not Found")
print(error_log("Zero Division"))
print(info_log())
-----
[{'source': 'error', 'message': 'File Not Found'}, {'source': 'error', 'message': 'Zero Division'}]
[{'source': 'info', 'message': 'Hello world'}]
In this iteration, we can now see that we have a new variable logs that is a list being saved in the outside function. This logs variable is saved to the scope of each creation of the closer, meaning that even though info_log and error_log both contain the list variable, they’re maintaining their own unique version of it (as seen in the print statements)
Understanding Scope: The LEGB Rule
Python’s way of dealing with variables is different than JS. Thing pre-ES6 days where we only had the var keyword and we had to be real careful with how we declared our variables. Nowadays those things are resolved by let and const, but Python does not have an equivalent. Instead, it assumes you know the LEGB rule and are applying it with your code. For a quick refresher:
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()
-----
local
enclosing
global
<built-in function len>
- Local – The function checks the local scope of the function to see if the variable was determined there
- Enclosing – This is for those hidden variables we talked about with closures. If the interpreter notices there’s a nested function, then it will check one level above to see if the variable was defined there
- Global – At first you might think, oh this is the rest of the file and widest scope, which is only half true. The first part is correct, it will check for any globally defined variables (either at the topmost scope of an import or outside of any function definitions). However, there’s actually one last step that is checked
-
Built-in – This is for the protected keywords, Python’s interpreter will check if the variable you’re trying to call is actually one that comes out of the box (like
len()) and will try to call that
Important: Python stops searching as soon as it finds the variable in any scope. It doesn’t keep looking in outer scopes once it finds a match.
The nonlocal Keyword
Example 5. make_avg_err.py: An example closure without a nonlocal variable
"""Before"""
def make_avg():
count = 0
total = 0
def inner(new_val):
count += 1
total += new_val
return total / count
return inner
avg = make_avg()
print(avg(10))
-----
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
This is where the nonlocal keyword comes into play. If you try to change a variable from the outer scope (e.g., count = count + 1), Python will freak out and say “I don’t know what count is!” (It thinks you are creating a new local variable).
Example 6. make_avg_fixed.py: Now using nonlocal to properly scope variables
"""After"""
def make_avg():
count = 0
total = 0
def inner(new_val):
nonlocal count, total
count += 1
total += new_val
return total / count
return inner
avg = make_avg()
print(avg(10))
-----
10.0
Now, this is only to be used when we’re dealing with immutable variables, like int, float, str, etc. Our example above with list does not need a nonlocal keyword because you’re not changing the variable, just the contents of it. list.append(item) simply adds a new item to the existing list, whereas counter += 1 is taking a new int and replacing the value of counter.
Wrap Up
Coming from a JS background it’s really easy to try and just do the same things in a diff language without understanding the nuances of the new code you’re writing. I’ve been building out this AST File Parser to go through all of the small things that make Python efficient. I’m hoping to wrap this project up by the end of the month so that I can be prepared for some fun MCP projects. Until then, will be happy to share some other findings in Python like Generators, Decorators, Special Methods, etc.
Top comments (0)