Closures in Python: How They Capture State
Table of Contents
A closure is a function that references variables from outside its own function body. The function definition and its environment are bundled together into a single entity, so it keeps track of some values from the place where it was defined, no matter where it's executed later on.
All the content from our Boot.dev courses are available for free here on the blog. This one is the "Closures" chapter of Learn Functional Programming in Python. If you want to try the far more immersive version of the course, do check it out!
How Does a Closure Capture State?
The concatter() function returns a function called doc_builder (yay higher-order functions!) that has a reference to an enclosed doc value.
from collections.abc import Callable
def concatter() -> Callable[[str], str]:
doc: str = ""
def doc_builder(word: str) -> str:
# "nonlocal" tells Python to use the 'doc'
# variable from the enclosing scope
nonlocal doc
doc += word + " "
return doc
return doc_builder
# save the returned 'doc_builder' function
# to the new function 'harry_potter_aggregator'
harry_potter_aggregator: Callable[[str], str] = concatter()
harry_potter_aggregator("Mr.")
harry_potter_aggregator("and")
harry_potter_aggregator("Mrs.")
harry_potter_aggregator("Dursley")
harry_potter_aggregator("of")
harry_potter_aggregator("number")
harry_potter_aggregator("four,")
harry_potter_aggregator("Privet")
print(harry_potter_aggregator("Drive"))
# Mr. and Mrs. Dursley of number four, Privet Drive
When concatter() is called, it creates a new "stateful" function that remembers the value of its internal doc variable. Each successive call to harry_potter_aggregator appends to that same doc!
It's as if you're saving the state of a function at a particular point in time, and then you can use and update that state later on.
What Does the nonlocal Keyword Do?
Python has a keyword called nonlocal that is required to modify a variable from an enclosing scope. Most programming languages don't require this keyword, but Python does.
When Should You Skip nonlocal?
When not to use the nonlocal keyword: when the variable is mutable – such as a list, dictionary, or set – and you're modifying its contents rather than reassigning the variable. You only need nonlocal if you're reassigning a variable (which you must do to update immutable values like strings and integers).
Let's try a closure without nonlocal.
from collections.abc import Callable
def new_collection(initial_docs: list[str]) -> Callable[[str], list[str]]:
docs: list[str] = initial_docs.copy()
def add_doc(doc: str) -> list[str]:
docs.append(doc)
return docs
return add_doc
my_collection: Callable[[str], list[str]] = new_collection(["doc1", "doc2", "doc3"])
print(my_collection("doc4"))
# ['doc1', 'doc2', 'doc3', 'doc4']
print(my_collection("doc5"))
# ['doc1', 'doc2', 'doc3', 'doc4', 'doc5']
Each time you call the returned function, it adds to the same list (the closure keeps track of the list's state).
Are Closures Pure Functions?
In many cases, no. The whole point of a closure is that it's stateful. It's a function that "remembers" the values from the enclosing scope even after the enclosing scope has finished executing.
That means that in many cases, closures are not pure functions. They can mutate state outside of their scope and have side effects. A closure retains the state of its environment, which makes it useful for tracking data as it changes over time, but it can come at the cost of understandability.
A Practical Closure Example
Here's a closure that adds CSS styling, keeping a growing dictionary of rules. Because we're dealing with nested dictionaries here, the .copy() method will produce a shallow copy: the outer dict is a new object, but mutating inner dicts will still affect the original one. So, you should import copy and use copy.deepcopy() instead.
import copy
from collections.abc import Callable
def css_styles(initial_styles: dict[str, dict[str, str]]) -> Callable[[str, str, str], dict[str, dict[str, str]]]:
styles: dict[str, dict[str, str]] = copy.deepcopy(initial_styles)
def add_style(selector: str, prop: str, value: str) -> dict[str, dict[str, str]]:
if selector not in styles:
styles[selector] = {}
styles[selector][prop] = value
return styles
return add_style
add_style: Callable[[str, str, str], dict[str, dict[str, str]]] = css_styles({"body": {"color": "black"}})
print(add_style("p", "color", "grey"))
# {'body': {'color': 'black'}, 'p': {'color': 'grey'}}
The returned add_style function carries the styles dictionary with it, building up state across calls.
Closures sit right alongside higher-order functions and the broader family of function types in functional Python. To go deeper, the Learn Functional Programming in Python course covers it, and the Learn Python course is a good place to firm up the function fundamentals first.
Frequently Asked Questions
What is a closure in Python?
A closure is a function that references variables from an enclosing scope, bundling the function definition together with its environment. It remembers those values even after the outer function has finished running, which lets it carry state between calls.
When do you need the nonlocal keyword in Python?
You need nonlocal when an inner function reassigns a variable that belongs to an enclosing function, such as a string or integer. Without it, the assignment creates a new local variable instead of updating the one from the outer scope.
What is the difference between nonlocal and global in Python?
nonlocal targets a variable in the nearest enclosing function scope, while global targets a variable at the module level. Use nonlocal inside nested functions and global when you need to modify a top-level variable.
Are closures pure functions?
Usually not. Closures are stateful by design and can mutate values from their enclosing scope, which means they often have side effects. A pure function returns the same output for the same input and changes nothing else.
Do you need nonlocal to modify a list inside a closure?
No. If you are mutating a mutable object like a list, dictionary, or set in place, the closure can do that through its reference without nonlocal. You only need nonlocal when reassigning the variable itself.
