Understanding Python Closures and Decorators with Practical Examples
This article explains the concepts of closures and decorators in Python, demonstrates how closures can capture and preserve state, and shows multiple decorator patterns—including simple, parameterized, and thread‑safe decorators—through clear code examples and explanations.
Python closures and decorators are essential functional‑programming tools that enable state encapsulation and dynamic modification of function behavior.
Closure
A closure is a function that can access variables defined in its enclosing scope, allowing it to remember and manipulate those variables even after the outer function has finished.
Simple closure example
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure = outer_function(10)
print(closure(5)) # Output: 15This example shows outer_function returning inner_function , which uses the variable x from the outer scope.
Using a closure to create a counter
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = create_counter()
print(counter()) # Output: 1
print(counter()) # Output: 2
print(counter()) # Output: 3Here the inner increment function retains the count variable across calls.
Decorator
A decorator is a special kind of closure that takes a function as an argument and returns a new function, typically used to augment or modify the original function without changing its source code.
Simple decorator
def simple_decorator(func):
def wrapper():
print("Before the function is called.")
func()
print("After the function is called.")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
# Output:
# Before the function is called.
# Hello!
# After the function is called.The @simple_decorator syntax is equivalent to say_hello = simple_decorator(say_hello) .
Decorator with parameters
def repeat(n_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(n_times=3)
def greet(name):
print(f"Hello {name}")
greet("Alice")
# Output:
# Hello Alice
# Hello Alice
# Hello AliceThe repeat decorator accepts a parameter n_times and repeats the wrapped function that many times.
Decorator handling optional arguments
def log(function=None, *, prefix=""):
def decorator(func):
def wrapper(*args, **kwargs):
print(prefix + "Calling function", func.__name__)
result = func(*args, **kwargs)
print(prefix + "Function finished")
return result
return wrapper
if function is not None:
return decorator(function)
return decorator
@log(prefix=">>> ")
def add(a, b):
return a + b
print(add(1, 2))
# Output:
# >>> Calling function add
# >>> Function finished
# 3This decorator can be used with or without explicit parameters.
Using a closure to implement caching
def cache(maxsize=128):
cache_dict = {}
def decorator(func):
def wrapper(*args):
if args in cache_dict:
print(f"Returning cached result for {args}")
return cache_dict[args]
if len(cache_dict) >= maxsize:
cache_dict.popitem(last=False)
result = func(*args)
cache_dict[args] = result
return result
return wrapper
return decorator
@cache(maxsize=10)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
# Output:
# Returning cached result for (10,)
# 55The cache decorator stores previously computed results to avoid redundant calculations.
Advanced closure example – Logger with state
def create_logger(log_level):
logs = []
def logger(message):
nonlocal logs
logs.append((log_level, message))
print(f"[{log_level}] {message}")
def get_logs():
return logs
return logger, get_logs
info_logger, info_get_logs = create_logger("INFO")
error_logger, error_get_logs = create_logger("ERROR")
info_logger("This is an info message.")
error_logger("This is an error message.")
print(info_get_logs()) # [('INFO', 'This is an info message.')]
print(error_get_logs()) # [('ERROR', 'This is an error message.')]This demonstrates a closure that maintains a mutable log list.
Dynamic class creation with a property
def create_class_with_property(property_name):
class DynamicClass:
def __init__(self):
self._property = None
@property
def prop(self):
return getattr(self, f"_{property_name}")
@prop.setter
def prop(self, value):
setattr(self, f"_{property_name}", value)
return DynamicClass
DynamicClassWithProperty = create_class_with_property("value")
instance = DynamicClassWithProperty()
instance.prop = 10
print(instance.prop) # Output: 10The function returns a class whose attribute name is defined at runtime.
Decorator chain (debug + retry)
def debug(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
def retry(times=3):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error: {e}")
raise Exception("Retry limit exceeded")
return wrapper
return decorator
@debug
@retry(times=2)
def risky_operation():
import random
if random.choice([True, False]):
return "Success"
else:
raise ValueError("Operation failed")
print(risky_operation())The combined decorators provide debugging output and automatic retries.
Parameterised decorator example
def parametrized_decorator(param):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Parameter: {param}")
return func(*args, **kwargs)
return wrapper
return decorator
@parametrized_decorator("hello")
def greet(name):
return f"Hello, {name}"
print(greet("Alice"))
# Output:
# Parameter: hello
# Hello, AliceThis shows how to pass arguments to a decorator factory.
Preserving function signatures with functools.wraps
from functools import wraps
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
@trace
def add(a, b):
"""Add two numbers"""
return a + b
print(add(1, 2))
print(add.__doc__) # Output: Add two numbersfunctools.wraps keeps the original function’s name and docstring.
Thread‑safe decorator
import threading
from functools import wraps
def thread_safe(func):
lock = threading.Lock()
@wraps(func)
def wrapper(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return wrapper
@thread_safe
def safe_increment(counter):
counter[0] += 1
return counter[0]
counter = [0]
print(safe_increment(counter)) # 1
print(safe_increment(counter)) # 2The decorator ensures that the wrapped function executes safely in multi‑threaded contexts.
Decorator factory (memoize)
def memoize(maxsize=128):
cache = {}
def decorator(func):
@wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
if len(cache) >= maxsize:
cache.popitem(last=False)
cache[args] = result
return result
return wrapper
return decorator
@memoize(maxsize=10)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # Output: 55This factory creates a memoization decorator with a configurable cache size.
Conclusion
Closures and decorators are powerful Python tools for encapsulating state, extending functionality, and writing cleaner code; experimenting with the patterns above will deepen understanding and improve practical programming skills.
Test Development Learning Exchange
Test Development Learning Exchange
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.