InterviewQAs

Python Decorators

Download as PDF
All questions in this page are included
Preparing…
Download PDF
PD
Python Decorators

Python decorators are widely used in production systems to add behavior around existing functions and methods without modifying their core implementation. They enable cross-cutting concerns such as logging, metrics collection, caching, validation, authorization, and transaction management.

Experienced engineers frequently use decorators to standardize behavior across large codebases. Instead of repeating the same logic in hundreds of functions, a well-designed decorator can centralize implementation and reduce maintenance overhead.

Understanding decorators goes beyond syntax. Real-world usage requires knowledge of closures, function metadata preservation, execution order, parameterized decorators, class decorators, and interactions with asynchronous code.

Decorator design involves tradeoffs. Poorly implemented decorators can obscure debugging, break introspection, introduce performance overhead, or interfere with testing. Strong implementations preserve signatures, maintain readability, and minimize side effects.

Question 01

Why are decorators preferred over copying the same logic into multiple functions when implementing logging, monitoring, or validation requirements?

EASY

Decorators provide a centralized mechanism for adding common behavior to many functions. Instead of duplicating logging, monitoring, validation, or authorization code throughout a codebase, the logic can be implemented once and applied wherever needed. This reduces maintenance effort and improves consistency.

In large systems, duplicated code often evolves differently across teams. One endpoint may log request IDs while another does not. A decorator helps enforce uniform behavior by wrapping all target functions with the same implementation.

Decorators also support separation of concerns. Business logic remains focused on solving the core problem while operational concerns such as auditing, tracing, and metrics collection remain isolated in reusable decorator modules.

Question 02

Which statements about functools.wraps are correct?

MEDIUM
  • A It preserves metadata such as function name and docstring.
  • B It automatically caches function results.
  • C It helps debugging and introspection tools identify the original function.
  • D It converts a decorator into an asynchronous decorator.

Without functools.wraps, a decorated function often appears as the wrapper function. This can affect debugging, documentation generation, tracing systems, and frameworks that inspect function metadata.

wraps does not provide caching or asynchronous behavior. Its primary purpose is metadata preservation and maintaining a clear relationship between the wrapper and the original function.

Question 03

Create a decorator that measures execution time and prints performance metrics for any function it wraps.

MEDIUM

This decorator records execution start time before calling the target function and calculates the elapsed time after execution. The finally block ensures timing information is recorded even if an exception occurs.

Performance monitoring decorators are commonly used in ETL pipelines, API gateways, batch processing systems, and data engineering workloads where identifying slow operations is critical.

# Python
import time
from functools import wraps


def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            duration = time.perf_counter() - start
            print(f"{func.__name__} executed in {duration:.6f} seconds")

    return wrapper


@measure_time
def generate_report():
    time.sleep(0.5)
    return "Report Generated"


print(generate_report())
Question 04

What challenges can arise when stacking multiple decorators on a single function?

MEDIUM

Stacked decorators execute in a specific order that is sometimes misunderstood. The decorator closest to the function executes first during wrapping, while execution flow during runtime moves through the outer wrappers before reaching the original function.

Incorrect ordering can create unexpected behavior. For example, applying caching before authorization might allow unauthorized users to receive cached results. Similarly, logging placed after exception-handling decorators may miss important failure details.

Production systems often require careful review of decorator order. Security, validation, transactions, monitoring, and caching layers should be arranged intentionally to ensure correctness and maintainability.

Question 05

Which decorator usage scenario best demonstrates a cross-cutting concern?

EASY
  • A Adding request logging to hundreds of API endpoints
  • B Changing a function's return value manually inside the function body
  • C Replacing a list with a tuple in local logic
  • D Renaming variables inside a function

Cross-cutting concerns are behaviors that apply across many parts of an application. Logging, authentication, auditing, monitoring, and tracing are common examples.

Decorators excel at implementing these concerns because they allow functionality to be added consistently without modifying individual business logic functions.

Question 06

Implement a parameterized decorator that retries a function a configurable number of times when exceptions occur.

MEDIUM

This implementation demonstrates a decorator factory. The outer function accepts configuration parameters and returns the actual decorator that wraps the target function.

Retry decorators are common in distributed systems where temporary failures occur when calling external APIs, message brokers, databases, or cloud services.

# Python
from functools import wraps
import time


def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None

            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last_error = exc
                    print(f"Attempt {attempt} failed: {exc}")
                    time.sleep(delay)

            raise last_error

        return wrapper

    return decorator


@retry(max_attempts=3, delay=2)
def connect_to_service():
    raise ConnectionError("Temporary network failure")


connect_to_service()
Question 07

Create a decorator that caches function results based on arguments without using functools.lru_cache.

HARD

The decorator stores previously computed results in a dictionary and returns cached values for repeated calls with identical arguments. This avoids unnecessary computation.

When implementing custom caching in production, engineers must consider memory growth, cache invalidation, thread safety, expiration policies, and argument serialization challenges.

# Python
from functools import wraps


def simple_cache(func):
    cache = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))

        if key not in cache:
            cache[key] = func(*args, **kwargs)

        return cache[key]

    return wrapper


@simple_cache
def expensive_calculation(x, y):
    print("Performing calculation")
    return x * y


print(expensive_calculation(5, 10))
print(expensive_calculation(5, 10))
Question 08

Which considerations are important when writing decorators for asynchronous functions?

HARD
  • A The wrapper may need to use async def and await.
  • B Blocking operations inside the decorator can affect event loop performance.
  • C The decorator should preserve coroutine behavior.
  • D Decorators automatically make synchronous functions asynchronous.

Asynchronous decorators must respect coroutine execution semantics. If the wrapped function is asynchronous, the wrapper generally needs to await the function rather than execute it like a normal callable.

Blocking operations such as heavy file I/O or long-running computations can stall the event loop and impact throughput. Preserving coroutine behavior is essential for compatibility with async frameworks and libraries.

Question 09

How would you evaluate whether a decorator-based solution is becoming too complex for a production codebase?

HARD

A decorator becomes problematic when understanding execution flow requires tracing through many layers of hidden behavior. If developers cannot easily determine why a function behaves a certain way, maintainability suffers.

Warning signs include deep decorator stacks, decorators that modify return types unexpectedly, extensive side effects, and business logic embedded inside infrastructure decorators. These patterns increase debugging difficulty and onboarding time.

A useful guideline is to keep decorators focused on a single concern. When a decorator starts handling validation, authorization, logging, caching, retries, and transformation simultaneously, a more explicit architectural approach may be easier to maintain.

Question 10

Implement a role-based authorization decorator that restricts function execution to users with required roles.

HARD

The decorator enforces authorization rules before allowing execution of protected functionality. The business logic remains clean because security concerns are separated into reusable components.

This pattern appears frequently in internal platforms, administrative portals, healthcare systems, financial applications, and API gateways where access policies must be applied consistently across many endpoints.

# Python
from functools import wraps


def require_role(required_role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if required_role not in user.get("roles", []):
                raise PermissionError(
                    f"Role '{required_role}' is required"
                )

            return func(user, *args, **kwargs)

        return wrapper

    return decorator


@require_role("admin")
def delete_account(user, account_id):
    return f"Account {account_id} deleted"


admin_user = {
    "name": "Alice",
    "roles": ["admin", "auditor"]
}

print(delete_account(admin_user, 1001))
Question 11

Explain the difference between a regular decorator and a parameterized decorator in Python.

EASY

A regular decorator takes a single function as input and returns a new function that wraps the original. It is applied directly with the @ syntax without passing extra arguments.

A parameterized decorator is a decorator factory. It takes additional arguments that configure the behavior of the decorator. The outer function returns the actual decorator which then wraps the target function.

Parameterization allows developers to reuse the same decorator logic with different settings, such as specifying a retry count, caching expiration, logging levels, or required roles, without duplicating code.

Question 12

Which of the following are potential pitfalls of using decorators in production code?

MEDIUM
  • A Obscuring function signatures and making debugging harder
  • B Creating unintended side effects if decorators modify state
  • C Automatically converting synchronous code to asynchronous
  • D Increasing call stack depth and potential performance overhead

Decorators can hide the original function metadata, making stack traces and debugging more challenging. This is why functools.wraps is important for preservation.

Modifying state within decorators can lead to unexpected behavior, particularly in shared mutable contexts or multi-threaded applications.

Each additional wrapper adds a layer to the call stack and can introduce slight performance overhead. While often negligible, excessive or complex decorators can accumulate noticeable latency.

Question 13

Write a decorator that logs both the input arguments and return value of a function.

MEDIUM

This decorator prints both the input parameters and the result, making it useful for debugging and monitoring in development or production.

Logging decorators are commonly used in API endpoints, ETL jobs, or background tasks to trace inputs and outputs without modifying business logic.

# Python
from functools import wraps


def log_io(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result

    return wrapper


@log_io
def add(x, y):
    return x + y


add(5, 7)
Question 14

How can decorators be used to enforce type checking in Python functions?

MEDIUM

Decorators can inspect input arguments using the inspect module or type annotations to verify that parameters match expected types before executing the function.

If a type mismatch is detected, the decorator can raise TypeError or log a warning, preventing runtime errors deeper in the code.

This approach allows centralized type enforcement, especially in systems where runtime type safety is important but static type checking tools like mypy are not sufficient.

Question 15

Which strategies help preserve function metadata and introspection when using decorators?

HARD
  • A Using functools.wraps inside the wrapper function
  • B Manually copying __name__ and __doc__ attributes from the original function
  • C Rewriting the function signature to match the original manually
  • D Decorators automatically preserve all metadata by default

wraps is the standard approach for preserving metadata such as __name__, __doc__, and module information.

Without it, introspection tools, documentation generators, and some frameworks may show the wrapper instead of the original function, complicating debugging.

Question 16

Implement a decorator that can be applied both to synchronous and asynchronous functions to log start and end times.

HARD

This decorator dynamically checks if the target function is a coroutine. It applies an async or sync wrapper accordingly, preserving performance monitoring for both types.

Such dual-purpose decorators are useful in frameworks like FastAPI where endpoints may be synchronous or asynchronous and consistent logging is required across all routes.

# Python
from functools import wraps
import asyncio
import time


def log_execution_time(func):
    if asyncio.iscoroutinefunction(func):
        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = await func(*args, **kwargs)
            duration = time.perf_counter() - start
            print(f"{func.__name__} executed in {duration:.6f} seconds")
            return result

        return async_wrapper
    else:
        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            duration = time.perf_counter() - start
            print(f"{func.__name__} executed in {duration:.6f} seconds")
            return result

        return sync_wrapper
Question 17

Describe how class decorators differ from function decorators and give a practical use case.

MEDIUM

Class decorators wrap entire classes rather than individual functions. They take the class as an input, optionally modify it or its methods, and return a new class or the same modified class.

Practical use cases include adding logging, validation, or caching to all methods in a class, registering classes in plugin frameworks, or enforcing interface compliance.

Unlike function decorators, class decorators can manipulate class-level attributes, inject methods dynamically, or wrap multiple methods with common behavior in a single place.

Question 18

Which of the following are valid ways to apply multiple decorators to a single function?

MEDIUM
  • A Stacking decorators using multiple @ lines above the function
  • B Chaining decorators manually by passing the function to each decorator
  • C Using functools.partial to combine decorators
  • D Placing decorators inside the function body

Decorators can be stacked using multiple @ lines. The decorator closest to the function wraps it first, and the outermost decorator wraps last.

Manual chaining is possible by explicitly passing the function through multiple decorator calls, achieving the same effect without syntactic sugar.

Question 19

Write a simple decorator that prints 'Function started' before executing any function and 'Function ended' after execution.

EASY

This basic decorator demonstrates wrapping a function to add pre- and post-execution behavior.

Such decorators are often used for lightweight tracing or debugging without changing the original function logic.

# Python
from functools import wraps


def simple_logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Function started")
        result = func(*args, **kwargs)
        print("Function ended")
        return result

    return wrapper


@simple_logger
def greet(name):
    print(f"Hello, {name}!")

greet("Vijay")
Question 20

Implement a decorator that limits the number of times a function can be called and raises an exception after the limit is reached.

HARD

The decorator tracks the number of times the function is called using a nonlocal counter variable. Once the limit is exceeded, it raises an exception to prevent further calls.

This pattern is useful for rate-limiting operations, controlling access to shared resources, or preventing accidental overuse of expensive computations.

# Python
from functools import wraps


def limit_calls(max_calls):
    def decorator(func):
        counter = 0

        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal counter
            if counter >= max_calls:
                raise RuntimeError(f"Function {func.__name__} call limit reached")
            counter += 1
            return func(*args, **kwargs)

        return wrapper

    return decorator


@limit_calls(3)
def say_hello():
    print("Hello")

say_hello()
say_hello()
say_hello()
say_hello()  # Raises RuntimeError
Question 21

Why should decorators generally avoid maintaining mutable global state?

MEDIUM

Mutable global state can introduce unpredictable behavior when multiple functions or threads access the same data. A decorator that stores counters, user information, or request details globally may produce inconsistent results under concurrent workloads.

Global state also makes testing more difficult. One test execution can influence another if state is not properly reset, leading to flaky and difficult-to-reproduce failures.

Production systems often favor dependency injection, thread-local storage, context variables, or explicitly managed caches instead of unmanaged global state inside decorators.

Question 22

Which decorator use cases commonly appear in enterprise API platforms?

MEDIUM
  • A Request auditing and tracing
  • B Authorization checks
  • C Rate limiting
  • D Database schema creation on every request

API gateways and backend services frequently use decorators to enforce security policies, collect telemetry, and control request volume.

Creating database schemas on every request is not a typical decorator use case and would introduce unnecessary overhead.

Question 23

Create a decorator that records how many times a function has been executed.

MEDIUM

The decorator maintains a closure variable that tracks invocation count. Each execution increments the counter before calling the original function.

This pattern is useful for diagnostics, lightweight monitoring, and validating execution frequency during testing.

# Python
from functools import wraps


def count_calls(func):
    count = 0

    @wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(f"{func.__name__} called {count} times")
        return func(*args, **kwargs)

    return wrapper


@count_calls
def process_record(record_id):
    return f"Processed {record_id}"


process_record(101)
process_record(102)
process_record(103)
Question 24

What design considerations are important when creating decorators for multi-threaded applications?

HARD

Thread safety is one of the primary concerns. If a decorator modifies shared state such as counters, caches, or configuration data, race conditions can occur when multiple threads execute simultaneously.

Synchronization mechanisms such as locks may be necessary, but excessive locking can reduce throughput and introduce contention. The decorator should balance correctness and performance.

Engineers should also evaluate whether state belongs inside the decorator at all. In many cases, external monitoring systems, thread-safe caches, or centralized services provide a more scalable solution.

Question 25

What does the following syntax represent? @my_decorator

EASY
  • A Applying a decorator to a function or class
  • B Creating a Python package
  • C Declaring a generator
  • D Importing a module

The @ syntax is decorator notation. It tells Python to pass the decorated object into the specified decorator and replace the original reference with the returned value.

It serves as syntactic sugar for explicitly wrapping the function after definition.

Question 26

Implement a decorator that blocks function execution outside business hours.

HARD

The decorator checks the current time before allowing execution. If the request occurs outside the permitted window, an exception is raised.

This pattern can support operational controls, administrative actions, or business workflows that must be restricted to specific time periods.

# Python
from functools import wraps
from datetime import datetime


def business_hours_only(start_hour=9, end_hour=17):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_hour = datetime.now().hour

            if not (start_hour <= current_hour < end_hour):
                raise PermissionError(
                    "Operation allowed only during business hours"
                )

            return func(*args, **kwargs)

        return wrapper

    return decorator


@business_hours_only()
def approve_invoice(invoice_id):
    return f"Invoice {invoice_id} approved"
Question 27

Which statements about decorator execution order are correct?

HARD
  • A The decorator closest to the function is applied first.
  • B The outermost decorator executes first during runtime entry.
  • C Decorator order never affects behavior.
  • D Changing decorator order can alter security and caching outcomes.

Decorator stacking follows a wrapping sequence where the closest decorator is applied first during decoration.

At runtime, execution enters through the outermost wrapper. Because wrappers can modify inputs, outputs, exceptions, or permissions, changing order can significantly affect behavior.

Question 28

Write a decorator that catches exceptions, logs them, and returns a default value.

MEDIUM

The decorator centralizes exception handling by catching errors and returning a fallback value instead of propagating exceptions.

Such decorators are useful when interacting with unstable external systems where graceful degradation is preferable to application failure.

# Python
from functools import wraps


def fallback(default_value=None):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as exc:
                print(f"Error: {exc}")
                return default_value

        return wrapper

    return decorator


@fallback(default_value="N/A")
def divide(a, b):
    return a / b


print(divide(10, 2))
print(divide(10, 0))
Question 29

When would a context manager be a better choice than a decorator?

MEDIUM

A context manager is often preferable when behavior should apply only to a specific block of code rather than an entire function. Examples include managing database transactions, file handles, locks, or temporary configuration changes.

Decorators operate at function or method boundaries, while context managers provide more granular control over execution scope.

If only part of a function requires special handling, a context manager usually provides clearer intent and better readability than wrapping the entire function with a decorator.

Question 30

Implement a decorator that measures execution time and stores the metric in a shared registry dictionary.

HARD

The decorator records execution duration and stores it in a central registry keyed by function name. This creates a simple metrics collection mechanism.

Real observability platforms extend this concept by exporting metrics to monitoring systems, enabling dashboards, alerts, and performance trend analysis.

# Python
from functools import wraps
import time


metrics_registry = {}


def collect_metrics(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()

        try:
            return func(*args, **kwargs)
        finally:
            duration = time.perf_counter() - start
            metrics_registry[func.__name__] = duration

    return wrapper


@collect_metrics
def generate_report():
    time.sleep(0.25)
    return "done"


generate_report()
print(metrics_registry)