Decorators & Closures β Python Questions with Answers
Start with the study notes on Decorators & Closures (definitions, examples and a quick revision), then work through 30 practice questions, each with the correct answer and a clear explanation of why it is right. When you are ready, take this topic as an interactive quiz to test yourself.
1 easy Β· 13 medium Β· 16 hard Β· Part of Python: Master Python from basics to advanced - 20 topics.
Topic overview
Decorators and Closures β functions that wrap and remember
1. Functions inside functions
Functions are first-class objects: they can be passed around, returned and defined inside other functions. A higher-order function takes or returns another function.
A closure is an inner function that remembers variables from the outer function, even after the outer function has finished.
def make_adder(n):
def add(x):
return x + n # n is remembered
return add
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(1), add10(1))
Output6 11
3. nonlocal
To change a variable of the outer function, the inner function needs nonlocal.
def counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
a = counter()
b = counter()
print(a(), a(), b()) # each counter has its own count
Output1 2 1
4. The late-binding trap
A closure remembers the variable, not its value at that moment. Lambdas made in a loop therefore all see the last value. Fix it with a default argument.
funcs = [lambda x: x * i for i in range(3)]
print([f(10) for f in funcs])
fixed = [lambda x, i=i: x * i for i in range(3)]
print([f(10) for f in fixed])
Output[20, 20, 20]
[0, 10, 20]
5. Decorators
A decorator takes a function and returns a new function that adds behaviour around it, without changing the original code. @deco above a def is shorthand for f = deco(f).
@functools.lru_cache: memoization, which caches results so repeated calls are instant.
@property, @staticmethod and @classmethod, on class methods.
Web frameworks, e.g. Flask's @app.route("/").
from functools import lru_cache
@lru_cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(50))
Output12586269025
Example: A class can also be a decorator by defining __call__, e.g. to count how many times a function is called.
10. Common misconceptions
Misconception
Correct understanding
A closure copies the value at creation time.
It remembers the variable; lambdas in a loop see its last value.
An inner function can change outer variables directly.
Changing an outer variable needs nonlocal.
A decorator runs every time before the function.
The decorator runs once at definition; the wrapper runs on each call.
A decorator should return wrapper().
It must return the function wrapper, not the result of calling it.
@a @b applies a first.
Decorators apply bottom-up: f = a(b(f)).
11. Quick revision
First-class function
Can be stored, passed and returned like any value.
Higher-order function
Takes or returns a function.
Closure
Inner function that remembers outer variables.
nonlocal
Lets an inner function change an outer variable.
Decorator
Wraps a function to add behaviour.
@deco
Shorthand for f = deco(f).
functools.wraps
Keeps the original function's name and docstring.
lru_cache
Caches results (memoization).
12. Test your understanding
What is a closure?
Show answer
An inner function that remembers variables from its enclosing function.
Which keyword lets an inner function change an outer variable?
Show answer
nonlocal
What does [f(10) for f in [lambda x: x * i for i in range(3)]] give?
Show answer
[20, 20, 20], because all lambdas see i = 2.
How do you fix that late-binding problem?
Show answer
Use a default argument: lambda x, i=i: x * i.
@deco above def f is shorthand for what?
Show answer
f = deco(f)
Why use *args and **kwargs in a wrapper?
Show answer
So it can accept and pass on any arguments.
What does functools.wraps do?
Show answer
Copies the original function's name and docstring onto the wrapper.
How many layers does a decorator with arguments need?
Show answer
Three: argument β decorator β wrapper.
Key takeaway: Closures remember outer variables, and decorators use that to wrap functions with extra behaviour; always pass *args/**kwargs and use functools.wraps.
Notes written by Velnov. Every code example with an output was run in Python 3 to check that the output shown is exactly what Python prints.
add = lambda a, b: a + b
print(sorted(data, key=lambda x: x[1]))
A A small anonymous function limited to a single expression
B A named recursive function
C A class method
D A loop construct
Answer: A β A small anonymous function limited to a single expression
lambda args: expression makes a one-line, unnamed function - handy inline where a short function is needed once, like a sort key. For anything longer, use def.
Answer: A β A function defined inside another function
Python lets you define a function inside another. The inner function is created fresh each time the outer runs, and it can see the outer function's variables.
ClosuresScope
Q5.medium
Why can a function be passed as an argument to another function?
A Functions are first-class objects - they can be stored in variables, passed around, and returned
B Because of a special decorator
C Only lambda functions can be passed
D They can't - only their results can
Answer: A β Functions are first-class objects - they can be stored in variables, passed around, and returned
In Python, functions are objects like ints or strings. You can assign them (f = print), put them in lists, pass them as arguments (sorted(key=len)), and return them from other functions.
A A function that takes another function as an argument and/or returns a function
B A function with many arguments
C A recursive function
D A function defined at the top of a file
Answer: A β A function that takes another function as an argument and/or returns a function
Higher-order functions operate on other functions. Examples: map(), filter(), sorted(key=...), and any function that returns a function (like decorators).
map()Decorators
Q7.medium
What does the @ symbol mean above a function definition?
@timer
def slow(): ...
@timer
def slow(): ...
# is exactly the same as:
def slow(): ...
slow = timer(slow)
A It applies a decorator - shorthand for slow = timer(slow)
B It's a comment
C It imports timer
D It makes slow private
Answer: A β It applies a decorator - shorthand for slow = timer(slow)
@timer is syntactic sugar. It runs the decorator timer with slow as its argument and rebinds the name slow to the result. The two forms are identical.
Decorator syntaxSyntactic sugar
Q14.medium
Which is a common REAL use for decorators?
A Logging, timing, caching, access control, retrying - cross-cutting concerns
Decorators shine for behavior you want around MANY functions without repeating it: @lru_cache (caching), Flask's @app.route (web routing), @staticmethod, timing, authentication.
Use casesDRY
Q16.medium
Can you stack multiple decorators on one function?
@a
@b
def f(): ...
@a
@b
def f(): ...
# equivalent to: f = a(b(f))
A Yes - they apply bottom-up: f = a(b(f))
B No, only one decorator per function
C Yes, but they apply top-down: f = b(a(f))
D Only if they take arguments
Answer: A β Yes - they apply bottom-up: f = a(b(f))
Decorators stack closest-first: b wraps f, then a wraps the result. Reading order matters - @a on top runs its 'before' code first at call time, but wraps last.
Decorator stackingOrder
Q19.medium
What is the output?
def power(base):
def raise_to(exp):
return base ** exp
return raise_to
square = power(2)
print(square(5))
A 32
B 10
C 25
D 7
Answer: A β 32
power(2) returns raise_to with base=2 remembered. square(5) computes 2 ** 5 = 32. A closure acting as a configurable function factory.
ClosuresFunction factories
Q20.medium
How can you inspect the values a closure has captured?
def outer():
x = 42
return lambda: x
f = outer()
print(f.__closure__[0].cell_contents) # 42
A func.__closure__ (each cell has .cell_contents)
B func.__vars__
C func.closure()
D You can't inspect them
Answer: A β func.__closure__ (each cell has .cell_contents)
The __closure__ attribute holds cell objects, one per captured variable, each exposing .cell_contents. Rarely needed, but proves closures really do store the values.
__closure__Introspection
Q21.medium
Why might you prefer a CLOSURE over a full CLASS for simple state?
A Less boilerplate when you only need one method plus some remembered state
B Closures are always faster
C Classes can't hold state
D Closures support inheritance
Answer: A β Less boilerplate when you only need one method plus some remembered state
For a single behavior over some captured data (a counter, a multiplier, a validator), a closure is lighter than a class with __init__ and one method. For richer state/behavior, a class is clearer.
Closures vs classesDesign
Q23.medium
What is the output ORDER?
@deco
def f(): print("body")
# where deco prints 'decorating' at wrap time
print("---")
f()
A decorating / --- / body (decorator runs at definition, before the call)
B --- / decorating / body
C body / decorating / ---
D --- / body
Answer: A β decorating / --- / body (decorator runs at definition, before the call)
The decorator runs ONCE when the function is defined (that's when @deco is applied) - printing 'decorating' immediately. The wrapper body runs later, when f() is called.
Decoration timeCall time
Q25.medium
Which built-in decorators are common on class methods?
A @property, @staticmethod, @classmethod
B @public, @private, @protected
C @method, @class, @object
D @get, @set, @delete
Answer: A β @property, @staticmethod, @classmethod
These three shape how methods work: @property exposes a method as an attribute, @staticmethod drops self, @classmethod passes cls. All are decorators applied above the method.
@property@staticmethod@classmethod
Q27.medium
What is the output?
def tag(name):
def deco(f):
def wrapper():
return f"<{name}>{f()}</{name}>"
return wrapper
return deco
@tag("b")
def text():
return "hi"
print(text())
A <b>hi</b>
B hi
C <b></b>
D <tag>hi</tag>
Answer: A β <b>hi</b>
tag("b") returns a decorator that wraps text(). Calling text() runs the wrapper: it calls the original (returning 'hi') and surrounds it with <b> tags -> '<b>hi</b>'. A parametrized decorator in action.
Parametrized decoratorsTracing
Q29.medium
Why can't a lambda contain statements like if/for or assignments?
grade = lambda s: "pass" if s >= 50 else "fail" # OK
# lambda s: for i in s: ... # SyntaxError
A A lambda body must be a single EXPRESSION - use def for anything with statements
B Lambdas are too slow for them
C They can, with semicolons
D Only in Python 2
Answer: A β A lambda body must be a single EXPRESSION - use def for anything with statements
Lambdas are limited to one expression that becomes the return value. You can use a conditional EXPRESSION (a if c else b), but not statements. Reach for def when you need real logic.
Lambda limitsdef
Hard questions (16)
Q2.hard
What is a CLOSURE?
def make_adder(n):
def add(x):
return x + n # remembers n
return add
add5 = make_adder(5)
print(add5(10)) # 15 - n=5 is remembered
A An inner function that remembers variables from its enclosing function, even after that function has finished
B A function that closes a file
C The end of a loop
D A private variable
Answer: A β An inner function that remembers variables from its enclosing function, even after that function has finished
When an inner function uses a variable from the outer function AND is returned, it keeps ('closes over') that variable alive - even after the outer function returns.
Nested functionsFree variables
Q3.hard
What is the output?
def outer():
x = 10
def inner():
return x
return inner
f = outer()
print(f())
f = outer() # outer finishes, but x lives on in f
print(f()) # 10
A 10
B None
C NameError - x is gone
D <function>
Answer: A β 10
Even though outer() has finished, the returned inner function still remembers x=10 through the closure. Calling f() returns 10.
ClosuresVariable lifetime
Q4.hard
Which keyword lets an inner function MODIFY a variable in the enclosing (outer) function?
nonlocal targets the nearest enclosing function's variable (not the global one). Without it, assigning inside the inner function would create a new local instead.
A A function that takes a function and returns a NEW function, usually adding behavior around it
B A comment style
C A type of loop
D A class attribute
Answer: A β A function that takes a function and returns a NEW function, usually adding behavior around it
A decorator wraps a function to add behavior (logging, timing, access checks) without changing the original function's code. It receives the function and returns a replacement.
ClosuresWrapping
Q9.hard
In a decorator's wrapper, why use *args and **kwargs?
def wrapper(*args, **kwargs):
def logged(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # pass all through
return wrapper
A So the wrapper can accept ANY arguments and pass them through to the wrapped function unchanged
B To make it faster
C They are required keywords for decorators
D To limit the function to two arguments
Answer: A β So the wrapper can accept ANY arguments and pass them through to the wrapped function unchanged
The decorator doesn't know what arguments the decorated function takes. *args/**kwargs let the wrapper accept everything and forward it: func(*args, **kwargs).
*args**kwargsDecorators
Q10.hard
What is the output?
def deco(f):
def wrapper():
print("before")
f()
print("after")
return wrapper
@deco
def hello():
print("hello")
hello()
# output:
# before
# hello
# after
A before / hello / after
B hello
C before / after
D hello / before / after
Answer: A β before / hello / after
@deco replaces hello with wrapper. Calling hello() now runs wrapper: prints 'before', calls the original hello ('hello'), then 'after'.
DecoratorsWrapping
Q11.hard
After @deco wraps hello(), what does hello.__name__ show WITHOUT functools.wraps?
A 'wrapper' - the wrapper's name replaces the original, losing metadata
B 'hello' - the name is preserved automatically
C 'deco'
D An empty string
Answer: A β 'wrapper' - the wrapper's name replaces the original, losing metadata
The decorated name now points to wrapper, so its __name__, __doc__ etc. are the wrapper's. This confuses debuggers and docs. functools.wraps copies the original's metadata onto the wrapper.
functools.wrapsMetadata
Q12.hard
What does functools.wraps do inside a decorator?
A Copies the original function's name, docstring and metadata onto the wrapper
B Wraps the function in a try/except
C Makes the function run twice
D Caches the result
Answer: A β Copies the original function's name, docstring and metadata onto the wrapper
@wraps(func) on the wrapper preserves func.__name__, __doc__ and __module__, so tools and help() still see the real function's identity. Best practice in every decorator.
functools.wrapsBest practices
Q13.hard
How can a decorator itself take arguments?
@repeat(3)
def greet(): ...
def repeat(n):
def decorator(func):
def wrapper(*a, **k):
for _ in range(n):
func(*a, **k)
return wrapper
return decorator
@repeat(3)
def hi(): print("hi") # prints hi 3 times
A You need THREE nested functions - the outer takes the argument and returns the actual decorator
B Decorators can't take arguments
C Use @repeat, 3
D Pass it to the wrapped function instead
Answer: A β You need THREE nested functions - the outer takes the argument and returns the actual decorator
@repeat(3) first CALLS repeat(3), which returns a decorator, which then wraps greet. That's three layers: repeat(n) -> decorator(func) -> wrapper(*args).
Parametrized decoratorsNested functions
Q15.hard
What does @functools.lru_cache do?
from functools import lru_cache
@lru_cache
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(50)) # instant, not slow
A Caches a function's results so repeated calls with the same arguments return instantly
B Limits how many times a function runs
C Logs the function calls
D Clears memory
Answer: A β Caches a function's results so repeated calls with the same arguments return instantly
lru_cache memoizes: it stores results by arguments. A slow recursive fib() becomes fast because repeated subproblems are looked up, not recomputed. LRU = Least Recently Used eviction when the cache is full.
MemoizationCaching
Q17.hard
What is the output?
def make_multipliers():
return [lambda x: x * i for i in range(3)]
print([m(10) for m in make_multipliers()])
# Fix with a default argument to capture the value:
[lambda x, i=i: x * i for i in range(3)]
# -> [0, 10, 20]
A [20, 20, 20] - all lambdas share the SAME i, which ends at 2
B [0, 10, 20]
C [0, 20, 40]
D [10, 10, 10]
Answer: A β [20, 20, 20] - all lambdas share the SAME i, which ends at 2
The lambdas close over the VARIABLE i, not its value at creation. By the time they run, the loop finished with i=2, so every lambda computes x*2 -> [20, 20, 20]. Classic closure trap.
Same root cause as the setTimeout(var i) trap in JavaScript.
Late bindingClosure traps
Q18.hard
How do you FIX late-binding so each lambda captures its own i?
[lambda x: x * i for i in range(3)]
mults = [lambda x, i=i: x * i for i in range(3)]
print([m(10) for m in mults]) # [0, 10, 20]
A Add a default argument: [lambda x, i=i: x * i for i in range(3)]
B Use global i
C Remove the loop
D It cannot be fixed
Answer: A β Add a default argument: [lambda x, i=i: x * i for i in range(3)]
Default arguments are evaluated at definition time, so i=i freezes the CURRENT value into each lambda. Now they give 0, 10, 20. (A factory function that takes i also works.)
Default argumentsLate binding
Q22.hard
A class-based decorator implements which method to make instances act as the wrapper?
The class stores the function in __init__ and defines __call__ so instances are callable. @MyDeco replaces the function with an instance; calling it runs __call__.
__call__Class decorators
Q24.hard
Why does a decorator usually RETURN the wrapper function instead of calling it?
A The decorator replaces the original name; it must return a callable to be called later, not the result of one call
B Returning is optional
C To save memory
D So the function runs immediately once
Answer: A β The decorator replaces the original name; it must return a callable to be called later, not the result of one call
@deco sets f = deco(f). If deco returned wrapper() (a result) instead of wrapper (the function), then f would be that result, and calling f() would fail. Return the function object.
DecoratorsCommon mistakes
Q26.hard
What is memoization (as done by a caching decorator)?
def memoize(f):
cache = {}
def wrapper(n):
if n not in cache:
cache[n] = f(n)
return cache[n]
return wrapper
A Storing results of expensive calls and reusing them for repeated inputs
B Writing memos in comments
C Freeing memory automatically
D Running a function in memory only
Answer: A β Storing results of expensive calls and reusing them for repeated inputs
Memoization trades memory for speed: a dict maps arguments to results, so a repeated call is a lookup, not a recomputation. A decorator is the clean way to add it. lru_cache does exactly this.
Cachinglru_cacheDynamic programming
Q30.hard
What is the output?
counter = 0
def make():
counter = 0
def inc():
nonlocal counter
counter += 1
return counter
return inc
a = make()
b = make()
print(a(), a(), b())
A 1 2 1 - each closure has its OWN independent counter
B 1 2 3 - they share one counter
C 1 1 1
D Error
Answer: A β 1 2 1 - each closure has its OWN independent counter
Each call to make() creates a fresh counter variable and a fresh inc closure over it. a and b are independent: a advances its own counter to 2, b's starts fresh at 1.