Closures & Higher-Order Functions
Closures & Higher-Order Functions
Functions as First-Class Objects
In Python, functions are first-class citizens — they can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions:
def greet(name):
return f"Hello, {name}!"
say_hello = greet # assign to variable
print(say_hello("Alice")) # Hello, Alice!
funcs = [abs, str, len] # store in a listHigher-Order Functions
A higher-order function either accepts a function as an argument or returns a function (or both).
Built-in Higher-Order Functions
nums = [1, -2, 3, -4, 5]
# map — apply function to every item
squares = list(map(lambda x: x**2, nums))
# filter — keep items where function returns True
positives = list(filter(lambda x: x > 0, nums))
# zip — pair items from two iterables
names = ["Alice", "Bob"]
scores = [95, 87]
paired = list(zip(names, scores)) # [("Alice", 95), ("Bob", 87)]
# enumerate — add index to iteration
for i, val in enumerate(["a", "b", "c"], start=1):
print(i, val)functools.reduce
reduce(f, iterable) collapses an iterable to a single value by applying f cumulatively:
from functools import reduce
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5]) # 120Closures
A closure is a nested function that remembers variables from its enclosing scope even after the outer function has returned:
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2The inner function counter closes over the variable count. Each call to make_counter() creates an independent closure with its own count.
The nonlocal Keyword
Without nonlocal, an assignment inside the inner function creates a new local variable, shadowing the outer one. nonlocal declares that a name refers to a variable in the nearest enclosing scope:
def outer():
x = 10
def inner():
nonlocal x
x += 5 # modifies outer's x
inner()
return x # 15Practical Closure Patterns
- Factories: generate specialized functions (e.g., multipliers, validators)
- Memoization: cache return values inside the enclosing scope
- Callbacks: pass state-carrying functions to event handlers
Code Examples
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map — square each number
squares = list(map(lambda x: x**2, numbers))
print("Squares:", squares)
# filter — keep only even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
print("Evens:", evens)
# zip — combine two lists element-wise
names = ["Alice", "Bob", "Carol"]
scores = [92, 85, 78]
results = list(zip(names, scores))
print("Results:", results)
# enumerate — index + value
print("\nLeaderboard:")
for rank, (name, score) in enumerate(results, start=1):
print(f" {rank}. {name}: {score}")map() and filter() return lazy iterators in Python 3, so list() is needed to materialise them. zip() stops at the shortest iterable. enumerate() with start= lets you control the initial index counter.
from functools import reduce
# reduce — fold a list into one value
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda acc, x: acc * x, numbers)
print("Product:", product)
total = reduce(lambda acc, x: acc + x, numbers, 0)
print("Sum:", total)
# Functions stored in a data structure
operations = {
"add": lambda a, b: a + b,
"subtract": lambda a, b: a - b,
"multiply": lambda a, b: a * b,
}
for op_name, func in operations.items():
print(f"10 {op_name} 3 = {func(10, 3)}")reduce() can take an optional initializer (third argument) used as the starting accumulator. Storing functions in a dict is a common pattern for dispatching operations without lengthy if/elif chains.
# Factory function returning a closure
def make_multiplier(factor):
def multiply(x):
return x * factor # 'factor' is closed over
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print("Double 7:", double(7))
print("Triple 7:", triple(7))
# Counter with nonlocal
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment
counter = make_counter(10)
print(counter()) # 11
print(counter(5)) # 16
print(counter()) # 17Each call to make_multiplier() creates a new closure with its own 'factor'. Without nonlocal, assigning to 'count' inside increment() would create a local variable, causing an UnboundLocalError on the += step.
Quick Quiz
1. What is a closure in Python?
2. What does the nonlocal keyword do?
3. What does filter(func, iterable) return?
Was this lesson helpful?