LearnFrenzy
Sign in Take a free test

Python Programming interview · Set 10 of 19

Decorators, Closures and Functional Programming

  • 6 follow-up questions
  • 4 key points
  • Code examples
Test yourself

Functions that wrap functions

Closures remember state, decorators add behaviour around calls, and functools supplies production-ready helpers. Interviewers expect you to write a decorator from scratch.

  • ClosuresCaptured variables and factories.
  • DecoratorsSimple, parameterised and stacked.
  • functoolswraps, lru_cache, partial and reduce.

Decorators appear in almost every framework — Flask routes, pytest fixtures, Django views, FastAPI endpoints — and “write a decorator” is a favourite live-coding task. Build them up from closures and they become simple.

1. What is a closure?

A closure is an inner function that remembers variables from the enclosing function's scope after that function has returned. Python stores those variables in the function's __closure__ cells. Closures are the building block of decorators and function factories.

def make_multiplier(factor):
    def multiply(x):
        return x * factor          # factor is remembered
    return multiply

double, triple = make_multiplier(2), make_multiplier(3)
print(double(5), triple(5))        # 10 15

2. Write a decorator that logs calls and explain each line.

A decorator takes a function, defines a wrapper that adds behaviour before and after calling it, and returns the wrapper. @log_calls above a function is shorthand for func = log_calls(func). Accept *args, **kwargs so it works for any signature, and use functools.wraps to keep the original name and docstring.

import functools, logging

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info("calling %s args=%s kwargs=%s", func.__name__, args, kwargs)
        result = func(*args, **kwargs)
        logging.info("%s returned %r", func.__name__, result)
        return result
    return wrapper

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

3. How do you write a decorator that takes arguments?

Add one more level: a function that takes the arguments and returns the actual decorator. A retry decorator is the classic example.

import functools, time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay * attempt)      # simple back-off
        return wrapper
    return decorator

@retry(times=5, exceptions=(ConnectionError,))
def fetch_rates():
    ...

4. In what order are stacked decorators applied?

Bottom-up when decorating, top-down when called. @a above @b means func = a(b(func)): b wraps first, and at call time a's wrapper runs first.

@timed          # applied second, runs first
@log_calls      # applied first, runs second
def build_report():
    ...
# equivalent to: build_report = timed(log_calls(build_report))

5. How does functools.lru_cache work and when is it unsafe?

It memoises results keyed by the arguments, so repeated calls with the same inputs return instantly. Arguments must be hashable. Do not cache functions with side effects or results that change over time (current prices, database reads) unless stale data is acceptable; use maxsize to bound memory.

from functools import lru_cache

@lru_cache(maxsize=1024)
def shipping_cost(pincode: str, weight_kg: int) -> float:
    ...                            # expensive calculation

shipping_cost.cache_info()          # hits, misses, size
shipping_cost.cache_clear()

6. Explain map, filter and reduce. Are they Pythonic?

map applies a function to each item, filter keeps items where it returns true, and functools.reduce folds a sequence into one value. They are lazy (map and filter return iterators). For simple cases comprehensions are usually more readable; built-ins such as sum, max and any often replace reduce.

from functools import reduce

prices = [120, 80, 250]
with_tax = list(map(lambda p: p * 1.18, prices))
expensive = list(filter(lambda p: p > 100, prices))
total = reduce(lambda a, b: a + b, prices)     # but sum(prices) is clearer

with_tax = [p * 1.18 for p in prices]          # usually preferred

7. What does functools.partial do?

It creates a new function with some arguments pre-filled — useful for callbacks and adapting functions to an expected signature.

from functools import partial

def send(channel, to, text):
    print(f"[{channel}] {to}: {text}")

send_sms = partial(send, "sms")
send_sms("Ravi", "Your OTP is 4821")

8. Give real-world uses of decorators.

Authentication and permission checks on web views, timing and logging, caching, retries, input validation, registering routes or plugins (@app.get("/users")), rate limiting, and transaction management. Class decorators such as @dataclass modify classes the same way.

ROUTES = {}

def route(path):
    def register(func):
        ROUTES[path] = func          # registration decorator
        return func
    return register

@route("/health")
def health():
    return "ok"

Remember

Key points

  • @d means f = d(f)

    Decoration happens at definition time.

  • Always use functools.wraps

    Keeps names, docstrings and introspection.

  • Arguments need an extra layer

    Factory → decorator → wrapper.

  • Cache only pure functions

    Hashable arguments, no side effects.

Interview drill

Follow-up questions

What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.

  1. 1 Why does a decorated function lose its name without wraps? Basic

    Because the decorator returns the wrapper function, whose __name__ is “wrapper”. functools.wraps copies the original metadata onto it.

  2. 2 Can a class be used as a decorator? Intermediate

    Yes — a class with __call__ can wrap a function and keep state (such as a call counter) in attributes.

  3. 3 How do you decorate an async function? Advanced

    The wrapper must be async def and await the original function; otherwise it returns an un-awaited coroutine.

    def timed_async(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            start = time.perf_counter()
            try:
                return await func(*args, **kwargs)
            finally:
                print(func.__name__, time.perf_counter() - start)
        return wrapper
  4. 4 What is the late-binding closure trap in loops? Intermediate

    [lambda: i for i in range(3)] all return 2, because each lambda looks up i when called. Bind the value with a default argument: lambda i=i: i.

  5. 5 What is the difference between cache and lru_cache? Basic

    functools.cache is an unbounded lru_cache(maxsize=None) — simpler and slightly faster, but it never evicts.

  6. 6 What is a pure function and why does it matter? Basic

    Its output depends only on its inputs and it has no side effects. Pure functions are easy to test, cache and run in parallel.

Preparing with friends?

Share this page with your study group or placement batch.

Popular