15 questions found
What is a decorator, and how does the @decorator_name syntax relate to a plain function call?
Beginner
A decorator is a function that takes another function as input and returns a (usually modified or wrapped) function as output; `@decorator_name` above a function definition is pure syntactic sugar for `func = decorator_name(func)`, applied immediately after the function is defined.
def shout(func):
def wrapper():
return func().upper()
return wrapper
@shout
def greet():
return "hello"
print(greet()) # "HELLO" -- greet was replaced by shout's wrapper
Real-world example
Automatically adding logging, timing, or access-control behavior to a function without modifying its internal code.
Common follow-ups: What is the exact equivalent, non-decorator-syntax way of writing '@shout' above 'def greet():'?
Functions & Scope
How do you write a basic decorator that adds a print statement before and after calling the decorated function?
Beginner
Define an outer function taking the target function as its parameter, define an inner 'wrapper' function that prints, calls the original function, and returns its result, then return the wrapper — the wrapper REPLACES the original function.
def logged(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} finished")
return result
return wrapper
@logged
def add(a, b):
return a + b
add(2, 3) # prints 'Calling add', then 'add finished'
Real-world example
Adding consistent logging around every call to a specific function without repeating the print statements manually each time.
Common follow-ups: Why does the wrapper function need to accept *args and **kwargs instead of specific named parameters?
Functions & Scope
Why should you use functools.wraps when writing a decorator, and what problem does it solve?
Intermediate
Without @functools.wraps(func), the wrapper function REPLACES the original's __name__, __doc__, and other metadata with the WRAPPER's own — breaking introspection tools, debuggers, and documentation generators that rely on a function's real identity; wraps() copies that metadata from the original function onto the wrapper.
import functools
def logged(func):
@functools.wraps(func) # preserves func's __name__, __doc__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@logged
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # 'add', NOT 'wrapper', thanks to functools.wraps
Real-world example
Ensuring a decorated function still shows its real name and docstring in help(), IDE tooltips, and automated documentation.
Common follow-ups: What specific attribute does functools.wraps ALSO preserve that lets you access the original undecorated function directly?
functools & Functional Programming Tools
How do you write a decorator that itself accepts arguments, like @retry(times=3)?
Intermediate
Add an EXTRA outer layer: a function that takes the decorator's OWN arguments and returns the actual decorator function (which in turn takes the target function and returns the wrapper) — three levels of nested functions total, letting you configure the decorator's behavior per use.
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise
return wrapper
return decorator
@retry(times=3)
def flaky_operation():
...
Real-world example
Building a configurable retry decorator that lets each usage specify its own number of retry attempts.
Common follow-ups: How would you make the 'times' argument to @retry optional, so @retry (without parentheses) also works?
Exception Handling
How do you write a decorator as a CLASS instead of a function, implementing __call__?
Intermediate
A class-based decorator stores the wrapped function (typically in __init__) and implements __call__ to actually invoke it — useful when the decorator needs to maintain PERSISTENT state (like a call counter) across multiple calls, more naturally expressed as instance attributes than closure variables.
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
print(f"Call #{self.calls} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def greet():
print("Hello!")
Real-world example
Building a decorator that needs to track state across every call, like counting invocations or caching results with explicit management.
Common follow-ups: Why does functools.update_wrapper() need to be used here instead of the simpler @functools.wraps decorator syntax?
Magic Methods & Operator Overloading
How do you write a decorator that works correctly on BOTH regular functions and methods (instance methods) of a class?
Advanced
Since Python's method binding automatically passes 'self' as the first positional argument when calling through an instance, a decorator using *args/**kwargs to pass arguments through transparently works identically for both plain functions and methods, WITHOUT needing any special-case handling — the decorator doesn't need to know or care whether it's wrapping a function or a method.
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): # 'self' arrives as args[0] automatically for methods
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Calculator:
@logged
def add(self, a, b): # works identically whether decorated as a function or a method
return a + b
Real-world example
Applying the same generic logging or timing decorator uniformly across both standalone functions and class methods.
Common follow-ups: Would this same decorator ALSO work correctly on a classmethod or staticmethod without modification?
OOP
How would you implement a caching/memoization decorator from scratch, similar to functools.lru_cache?
Advanced
Maintain a dictionary (closure variable or instance attribute) mapping argument tuples to previously-computed results; the wrapper checks the cache first and only calls the real function (storing the new result) on a cache miss — must handle unhashable arguments carefully, since dict keys require hashability.
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2: return n
return fibonacci(n - 1) + fibonacci(n - 2)
Real-world example
Building a custom caching decorator with specialized eviction logic that functools.lru_cache's simple LRU policy doesn't support.
Common follow-ups: What happens if this memoize decorator is applied to a function called with UNHASHABLE arguments, like a list?
Data Types & Structures
How do you stack MULTIPLE decorators on the same function, and in what order do they actually execute?
Advanced
Decorators are applied BOTTOM-UP (the one closest to the function definition wraps first), but they EXECUTE top-down at call time, since each decorator's wrapper calls the NEXT one inward — meaning the outermost decorator's 'before' code runs first, and its 'after' code runs last.
def bold(func):
def wrapper(): return f"<b>{func()}</b>"
return wrapper
def italic(func):
def wrapper(): return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def text():
return "Hello"
print(text()) # '<b><i>Hello</i></b>' -- italic applied first (closest), then bold wraps around it
Real-world example
Combining multiple decorators, like @app.route() and @login_required, and understanding the resulting call order.
Common follow-ups: How would swapping the order of @bold and @italic change the final output?
Functions & Scope
How does functools.singledispatch let you implement function overloading based on an argument's TYPE?
Advanced
@functools.singledispatch turns a function into a GENERIC function that dispatches to different implementations based on the type of its FIRST argument — register additional type-specific implementations with @function_name.register(Type), and the correct one is chosen automatically at call time.
from functools import singledispatch
@singledispatch
def describe(value):
return f"A value: {value}"
@describe.register
def _(value: int):
return f"An integer: {value}"
@describe.register
def _(value: list):
return f"A list of {len(value)} items"
print(describe(42)) # "An integer: 42"
print(describe([1,2,3])) # "A list of 3 items"
Real-world example
Implementing type-based dispatch for a serialization or rendering function that behaves differently based on input type.
Common follow-ups: How does functools.singledispatchmethod extend this same capability to instance methods inside a class?
Type Hints
How would you write a decorator that validates a function's arguments against its type hints at RUNTIME, since Python doesn't enforce type hints by default?
Advanced
Inspect the function's __annotations__ dict inside the wrapper, and for each parameter with a type hint, check the actual passed argument's type using isinstance() (typically via inspect.signature() to correctly map positional/keyword args to their parameter names), raising a TypeError on mismatch.
import inspect, functools
def enforce_types(func):
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
for name, value in bound.arguments.items():
expected = func.__annotations__.get(name)
if expected and not isinstance(value, expected):
raise TypeError(f"{name} must be {expected}, got {type(value)}")
return func(*args, **kwargs)
return wrapper
@enforce_types
def add(a: int, b: int) -> int:
return a + b
Real-world example
Adding runtime type safety to a critical internal function's inputs in a codebase without adopting a full static type-checking pipeline.
Common follow-ups: Why might this manual approach be preferred over (or a stepping stone toward) a dedicated library like pydantic for this purpose?
Type Hints