Decorators in Python - Interview Questions and Answers

A decorator in Python is a function that modifies the behavior of another function or class method without modifying its code. It allows adding functionality dynamically.

def my_decorator(func):
    def wrapper():
        print("Something before function execution")
        func()
        print("Something after function execution")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:

Something before function execution
Hello!
Something after function execution

 

Decorators are used for logging, enforcing access control, memoization, authentication, performance measurement, etc.

You can stack multiple decorators:

@decorator1
@decorator2
def my_function():
    pass

The order of execution is decorator2 first, then decorator1.

Yes, by wrapping it inside another function:

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

@repeat(3)
def greet():
    print("Hello!")

greet()

Output:

Hello!
Hello!
Hello!

 

It preserves the original function’s metadata when applying a decorator:

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

 

class MyClass:
    @staticmethod
    @my_decorator
    def my_method():
        print("Inside method")

 

  • Function decorators modify functions.
  • Class decorators modify class behavior.

class MyDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("Before function call")
        result = self.func(*args, **kwargs)
        print("After function call")
        return result

@MyDecorator
def greet():
    print("Hello!")

greet()

 

Yes, using @classmethod, @staticmethod, or custom decorators.

By encapsulating common functionalities like logging, caching, and validation without modifying function logic.

Yes, but typically they return a function.

By calling the original function directly (func.__wrapped__ if functools.wraps is used).

A decorator that caches results to improve performance.

from functools import lru_cache

@lru_cache(maxsize=100)
def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

 

It will cause a TypeError when the decorated function is called.

Yes, by catching exceptions inside the wrapper function.

def modify_args(func):
    def wrapper(x):
        return func(x * 2)
    return wrapper

@modify_args
def print_num(n):
    print(n)

print_num(5)  # Outputs 10

 

Decorators that accept arguments to modify behavior.

A decorator that logs function calls and arguments:

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

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

 

Yes, by changing the return value in the wrapper.

@staticmethod is a built-in decorator that defines a method inside a class that doesn’t operate on an instance or class itself. It behaves like a regular function inside the class.

class MathOperations:
    @staticmethod
    def add(a, b):
        return a + b

print(MathOperations.add(5, 3))  # Output: 8

 

@classmethod is a decorator that allows a method to operate on the class itself instead of an instance. It takes cls as the first parameter.

class MyClass:
    class_var = "Hello"

    @classmethod
    def print_class_var(cls):
        print(cls.class_var)

MyClass.print_class_var()  # Output: Hello

 

Yes, a decorator can modify instance attributes by accessing self if applied to instance methods.

def modify_instance(func):
    def wrapper(self, *args, **kwargs):
        self.name = "Modified"
        return func(self, *args, **kwargs)
    return wrapper

class Example:
    def __init__(self, name):
        self.name = name

    @modify_instance
    def show(self):
        print(self.name)

obj = Example("Original")
obj.show()  # Output: Modified

 

If a decorator doesn’t return a function, it causes an error when trying to call the decorated function.

def invalid_decorator(func):
    return "This is not a function"

@invalid_decorator
def test():
    print("Hello")

test()  # TypeError: 'str' object is not callable

 

Decorators can be applied dynamically by assigning them at runtime.

def decorator1(func):
    def wrapper():
        print("Decorator1")
        func()
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator2")
        func()
    return wrapper

def my_func():
    print("Original function")

my_func = decorator1(decorator2(my_func))
my_func()

Output:

Decorator1  
Decorator2  
Original function  

 

A timing decorator can be used to measure the execution time of a function.

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Execution time: {end - start:.5f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)
    print("Finished")

slow_function()

 

Yes, but you need to declare them global inside the decorator.

counter = 0

def increment_counter(func):
    def wrapper():
        global counter
        counter += 1
        return func()
    return wrapper

@increment_counter
def greet():
    print("Hello")

greet()
print(counter)  # Output: 1

 

Using functools.wraps preserves the original function signature.

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def example(a, b):
    return a + b

print(example.__name__)  # Output: example

 

Yes, by calling the original function directly if functools.wraps is used.

@my_decorator
def example():
    print("Hello")

print(example.__wrapped__())  # Calls the undecorated function

 

Use an if statement before applying decorators.

use_logging = True

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Logging {func.__name__}")
        return func(*args, **kwargs)
    return wrapper if use_logging else func

@log_decorator
def my_function():
    print("Function executed")

my_function()

 

Lambdas can be decorated just like regular functions.

@log_decorator
lambda_func = lambda x: x * 2
print(lambda_func(5))  # Output: Logging <lambda> 10

 

Yes, a decorator can be defined inside another function.

def outer():
    def decorator(func):
        def wrapper():
            print("Decorator inside outer")
            return func()
        return wrapper
    return decorator

@outer()
def say_hello():
    print("Hello!")

say_hello()

 

Wrap the decorator inside another function that accepts parameters.

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

@repeat(3)
def greet():
    print("Hello!")

greet()

 

Yes, but it requires extra handling since built-ins lack __name__ attributes.

It can make testing harder by modifying function behavior. Using @wraps helps preserve function metadata.

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

 

A Django decorator that checks user permissions before executing a view.

from django.contrib.auth.decorators import login_required

@login_required
def dashboard(request):
    pass

 

import asyncio

def async_decorator(func):
    async def wrapper(*args, **kwargs):
        print("Before async function")
        result = await func(*args, **kwargs)
        print("After async function")
        return result
    return wrapper

@async_decorator
async def my_async_function():
    print("Inside async function")

asyncio.run(my_async_function())

 

import time

def retry(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    time.sleep(1)
        return wrapper
    return decorator

@retry(3)
def fetch_data():
    print("Fetching data...")
    raise ValueError("Network error!")

fetch_data()

 

Decorators can enforce authentication and validation before executing a function.

A class-based decorator defines __call__ to make the class behave like a function.

class Logger:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"Logging: {self.func.__name__}")
        return self.func(*args, **kwargs)

@Logger
def greet():
    print("Hello!")

greet()

Output:

Logging: greet  
Hello!  

 

Yes, using instance attributes.

class Counter:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call count: {self.count}")
        return self.func(*args, **kwargs)

@Counter
def hello():
    print("Hello!")

hello()
hello()

Output:

Call count: 1  
Hello!  
Call count: 2  
Hello!  

 

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Arguments: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"Return Value: {result}")
        return result
    return wrapper

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

add(3, 4)

 

def limit_calls(max_calls):
    def decorator(func):
        count = 0
        def wrapper(*args, **kwargs):
            nonlocal count
            if count >= max_calls:
                print("Function call limit reached!")
                return
            count += 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()  # Will not execute

 

FeatureFunction-based DecoratorClass-based Decorator
SimplicityEasier to writeMore complex
State PersistenceHarder to store stateCan store state easily
ReusabilityLess flexibleMore flexible
__call__ methodNot requiredRequired

Yes, but yield should be handled properly.

def generator_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before generator starts")
        yield from func(*args, **kwargs)
        print("After generator ends")
    return wrapper

@generator_decorator
def count():
    yield 1
    yield 2
    yield 3

for num in count():
    print(num)

 

Use asyncio.iscoroutinefunction to check for async functions.

import asyncio
import functools

def universal_decorator(func):
    @functools.wraps(func)
    async def async_wrapper(*args, **kwargs):
        print("Async function detected")
        return await func(*args, **kwargs)

    def sync_wrapper(*args, **kwargs):
        print("Sync function detected")
        return func(*args, **kwargs)

    return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper

@universal_decorator
def sync_func():
    print("I am sync!")

@universal_decorator
async def async_func():
    print("I am async!")

sync_func()
asyncio.run(async_func())

 

They modify class behavior using metaclasses.

class MetaDecorator(type):
    def __new__(cls, name, bases, dct):
        dct['added_method'] = lambda self: "Added by metaclass"
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=MetaDecorator):
    pass

obj = MyClass()
print(obj.added_method())  # Output: Added by metaclass

 

import time

def profile(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Execution Time: {end - start:.4f} sec")
        return result
    return wrapper

@profile
def slow_task():
    time.sleep(2)
    print("Task completed")

slow_task()

 

def validate_positive(func):
    def wrapper(*args, **kwargs):
        if any(arg < 0 for arg in args):
            raise ValueError("Negative values not allowed")
        return func(*args, **kwargs)
    return wrapper

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

print(add(3, 5))  # Works
# print(add(-1, 2))  # Raises ValueError

 

Yes, by changing the function's output before returning.

def modify_return(func):
    def wrapper(*args, **kwargs):
        return f"Modified: {func(*args, **kwargs)}"
    return wrapper

@modify_return
def greet():
    return "Hello"

print(greet())  # Output: Modified: Hello

 

  • @app.route("/") → Define routes
  • @login_required → Enforce authentication
  • @cache → Cache responses

from functools import lru_cache

@lru_cache(maxsize=10)
def expensive_calculation(n):
    print("Computing...")
    return n * n

print(expensive_calculation(5))
print(expensive_calculation(5))  # Cached result

 

def exception_handler(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"Error: {e}")
    return wrapper

@exception_handler
def divide(a, b):
    return a / b

print(divide(10, 0))  # Output: Error: division by zero

 

A function that returns a 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 say_hi():
    print("Hi!")

say_hi()

 

Store execution details in a log file or global list.

Yes, to modify operator behavior in classes.

def doc_modifier(new_doc):
    def decorator(func):
        func.__doc__ = new_doc
        return func
    return decorator

@doc_modifier("New documentation")
def example():
    """Old doc"""
    pass

print(example.__doc__)  # Output: New documentation

 

They can ensure thread safety by synchronizing access.

Yes, by restoring the original function reference.

A dynamic decorator factory is a function that generates decorators dynamically based on input arguments.

def dynamic_decorator(prefix):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"{prefix}: Executing {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@dynamic_decorator("INFO")
def greet():
    print("Hello!")

greet()

 

Using a time-based check to limit function calls.

import time

def rate_limiter(max_calls, period):
    calls = []

    def decorator(func):
        def wrapper(*args, **kwargs):
            now = time.time()
            calls[:] = [t for t in calls if now - t < period]
            if len(calls) >= max_calls:
                raise Exception("Rate limit exceeded!")
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limiter(3, 5)  # Max 3 calls per 5 seconds
def api_request():
    print("API request sent")

api_request()

 

Yes, but yield should be handled properly.

def generator_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before generator starts")
        yield from func(*args, **kwargs)
        print("After generator ends")
    return wrapper

@generator_decorator
def count():
    yield 1
    yield 2
    yield 3

for num in count():
    print(num)

 

def type_enforced(func):
    def wrapper(*args):
        if not all(isinstance(arg, int) for arg in args):
            raise TypeError("Only integers allowed!")
        return func(*args)
    return wrapper

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

print(add(3, 5))  # Works
# print(add(3, "5"))  # Raises TypeError

 

Decorators can be used to intercept function calls, similar to middleware in web frameworks.

def middleware(func):
    def wrapper(*args, **kwargs):
        print("Middleware: Pre-processing request")
        response = func(*args, **kwargs)
        print("Middleware: Post-processing response")
        return response
    return wrapper

@middleware
def handler():
    print("Handling request...")

handler()

 

Yes, using functools.wraps to preserve the original name.

import functools

def change_name(new_name):
    def decorator(func):
        func.__name__ = new_name
        return func
    return decorator

@change_name("custom_function")
def test():
    pass

print(test.__name__)  # Output: custom_function

 

Use unittest and mock decorators if needed.

import unittest

def debug(func):
    def wrapper(*args, **kwargs):
        print("Debugging...")
        return func(*args, **kwargs)
    return wrapper

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

class TestDecorator(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)

unittest.main()

 

Django provides @transaction.atomic to ensure database consistency.

from django.db import transaction

@transaction.atomic
def update_user():
    # All operations inside this function are either committed or rolled back
    pass

 

Flask uses decorators to define routes.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def home():
    return "Hello, Flask!"

 

FastAPI uses decorators for API routing.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello FastAPI"}

 

Yes, decorators work normally in Jupyter notebooks.

wrapt ensures correctness in complex decorators by preserving function metadata.

They modify class behavior using metaclasses.

class MetaDecorator(type):
    def __new__(cls, name, bases, dct):
        dct['extra_method'] = lambda self: "Added by metaclass"
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=MetaDecorator):
    pass

obj = MyClass()
print(obj.extra_method())  # Output: Added by metaclass

 

@dataclass is implemented using the dataclasses module.

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

 

Function attributes store metadata inside decorators.

def add_metadata(func):
    func.version = "1.0"
    return func

@add_metadata
def test():
    pass

print(test.version)  # Output: 1.0

 

Yes, by injecting dependencies dynamically.

Decorators can alter MRO by modifying __mro__.

Yes, by dynamically changing the class’s base classes.

def modify_base(cls):
    cls.__bases__ = (NewBaseClass,)
    return cls

 

Use function attributes or functools.wraps.

They simplify resource management.

from contextlib import contextmanager

@contextmanager
def open_file(name):
    f = open(name, 'w')
    try:
        yield f
    finally:
        f.close()

 

Share   Share