πŸŽ“ Velnov

🐍 Python · Topic 18 of 20

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.

def apply_twice(f, x):
    return f(f(x))

print(apply_twice(lambda n: n + 3, 10))
Output16

2. Closures

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).

def deco(func):
    def wrapper():
        print("before")
        func()
        print("after")
    return wrapper

@deco
def hello():
    print("hello")

hello()
Outputbefore
hello
after

6. A proper decorator: *args, **kwargs and wraps

import functools

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@logged
def add(a, b):
    return a + b

print(add(2, 3))
print(add.__name__)
Outputcalling add
5
add

Remember: Without functools.wraps, add.__name__ would say wrapper.

7. Decorators with arguments

@repeat(3) needs three layers: the outer function takes the argument and returns the real decorator.

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def hi():
    print("hi")

hi()
Outputhi
hi
hi

8. Stacking and timing

def tag(name):
    def deco(f):
        def wrapper():
            return f"<{name}>{f()}</{name}>"
        return wrapper
    return deco

@tag("b")
@tag("i")
def text():
    return "hi"

print(text())
Output<b><i>hi</i></b>

9. Decorators you will meet

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

MisconceptionCorrect 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

  1. What is a closure?

    Show answer

    An inner function that remembers variables from its enclosing function.

  2. Which keyword lets an inner function change an outer variable?

    Show answer

    nonlocal

  3. 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.

  4. How do you fix that late-binding problem?

    Show answer

    Use a default argument: lambda x, i=i: x * i.

  5. @deco above def f is shorthand for what?

    Show answer

    f = deco(f)

  6. Why use *args and **kwargs in a wrapper?

    Show answer

    So it can accept and pass on any arguments.

  7. What does functools.wraps do?

    Show answer

    Copies the original function's name and docstring onto the wrapper.

  8. 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.

Key concepts in this topic

ClosuresScopeNested functionsFree variablesVariable lifetimenonlocalglobalFirst-class functionsHigher-order functionsmap()DecoratorsDecorator syntaxSyntactic sugarWrapping*args**kwargsfunctools.wrapsMetadataBest practicesParametrized decoratorsUse casesDRYMemoizationCaching

Jump to: Easy Β· Medium Β· Hard

Easy questions (1)

Q28. easy

A lambda is best described as:

add = lambda a, b: a + b
print(sorted(data, key=lambda x: x[1]))
  1. A A small anonymous function limited to a single expression
  2. B A named recursive function
  3. C A class method
  4. 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.

LambdaAnonymous functions

Medium questions (13)

Q1. medium

What is a nested function in Python?

def outer():
    def inner():
        print("inside")
    inner()

outer()   # inside
  1. A A function defined inside another function
  2. B A function that calls itself
  3. C Two functions with the same name
  4. D A function imported from another module

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?

def shout(t): return t.upper()
funcs = [shout, str.lower]
print(funcs[0]("hi"))   # HI
  1. A Functions are first-class objects - they can be stored in variables, passed around, and returned
  2. B Because of a special decorator
  3. C Only lambda functions can be passed
  4. 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.

First-class functionsHigher-order functions

Q6. medium

What is a higher-order function?

def apply_twice(f, x):
    return f(f(x))

print(apply_twice(lambda n: n + 3, 10))   # 16
  1. A A function that takes another function as an argument and/or returns a function
  2. B A function with many arguments
  3. C A recursive function
  4. 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)
  1. A It applies a decorator - shorthand for slow = timer(slow)
  2. B It's a comment
  3. C It imports timer
  4. 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?

  1. A Logging, timing, caching, access control, retrying - cross-cutting concerns
  2. B Declaring variables
  3. C Importing modules
  4. D Creating loops

Answer: 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))
  1. A Yes - they apply bottom-up: f = a(b(f))
  2. B No, only one decorator per function
  3. C Yes, but they apply top-down: f = b(a(f))
  4. 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))

  1. A 32
  2. B 10
  3. C 25
  4. 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
  1. A func.__closure__ (each cell has .cell_contents)
  2. B func.__vars__
  3. C func.closure()
  4. 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?

  1. A Less boilerplate when you only need one method plus some remembered state
  2. B Closures are always faster
  3. C Classes can't hold state
  4. 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()

  1. A decorating / --- / body (decorator runs at definition, before the call)
  2. B --- / decorating / body
  3. C body / decorating / ---
  4. 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?

  1. A @property, @staticmethod, @classmethod
  2. B @public, @private, @protected
  3. C @method, @class, @object
  4. 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())

  1. A <b>hi</b>
  2. B hi
  3. C <b></b>
  4. 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
  1. A A lambda body must be a single EXPRESSION - use def for anything with statements
  2. B Lambdas are too slow for them
  3. C They can, with semicolons
  4. 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
  1. A An inner function that remembers variables from its enclosing function, even after that function has finished
  2. B A function that closes a file
  3. C The end of a loop
  4. 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
  1. A 10
  2. B None
  3. C NameError - x is gone
  4. 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?

def counter():
    count = 0
    def inc():
        nonlocal count
        count += 1
        return count
    return inc

c = counter()
print(c(), c(), c())   # 1 2 3
  1. A nonlocal
  2. B global
  3. C outer
  4. D enclosing

Answer: A β€” nonlocal

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.

nonlocalglobalClosures

Q8. hard

What is a decorator?

def logged(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@logged
def greet(): print("hi")
  1. A A function that takes a function and returns a NEW function, usually adding behavior around it
  2. B A comment style
  3. C A type of loop
  4. 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
  1. A So the wrapper can accept ANY arguments and pass them through to the wrapped function unchanged
  2. B To make it faster
  3. C They are required keywords for decorators
  4. 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
  1. A before / hello / after
  2. B hello
  3. C before / after
  4. 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?

from functools import wraps
def deco(f):
    @wraps(f)          # preserves f's name/doc
    def wrapper(*a, **k):
        return f(*a, **k)
    return wrapper
  1. A 'wrapper' - the wrapper's name replaces the original, losing metadata
  2. B 'hello' - the name is preserved automatically
  3. C 'deco'
  4. 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?

  1. A Copies the original function's name, docstring and metadata onto the wrapper
  2. B Wraps the function in a try/except
  3. C Makes the function run twice
  4. 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
  1. A You need THREE nested functions - the outer takes the argument and returns the actual decorator
  2. B Decorators can't take arguments
  3. C Use @repeat, 3
  4. 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
  1. A Caches a function's results so repeated calls with the same arguments return instantly
  2. B Limits how many times a function runs
  3. C Logs the function calls
  4. 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]
  1. A [20, 20, 20] - all lambdas share the SAME i, which ends at 2
  2. B [0, 10, 20]
  3. C [0, 20, 40]
  4. 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]
  1. A Add a default argument: [lambda x, i=i: x * i for i in range(3)]
  2. B Use global i
  3. C Remove the loop
  4. 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?

class CountCalls:
    def __init__(self, f):
        self.f = f
        self.n = 0
    def __call__(self, *a, **k):
        self.n += 1
        return self.f(*a, **k)
  1. A __call__
  2. B __wrap__
  3. C __deco__
  4. D __init__ only

Answer: A β€” __call__

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?

  1. A The decorator replaces the original name; it must return a callable to be called later, not the result of one call
  2. B Returning is optional
  3. C To save memory
  4. 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
  1. A Storing results of expensive calls and reusing them for repeated inputs
  2. B Writing memos in comments
  3. C Freeing memory automatically
  4. 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())

  1. A 1 2 1 - each closure has its OWN independent counter
  2. B 1 2 3 - they share one counter
  3. C 1 1 1
  4. 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.

ClosuresIndependent state

Practice β€œDecorators & Closures” as a quiz