Python Decorators Explained with Examples: Complete Beginner to Advanced Guide

Decorators are one of Python’s most powerful features—and also one of the most commonly asked topics in Python interviews.

Whether you’re learning Python for the first time, preparing for a FastAPI or Django interview, or trying to understand how frameworks like Flask and FastAPI work internally, decorators are an essential concept to master.

You have probably encountered code like this:

@app.get("/users")<br>def get_users():<br>return users

or

@login_required<br>def dashboard():<br>pass

These lines may look magical at first, but they are built on a simple and elegant Python feature called decorators

Learn Python decorators from scratch with practical examples, common mistakes, interview questions, closures, wraps(), decorator arguments, and real-world FastAPI use cases.

Python Decorators

Table of Contents

  1. What Is a Decorator?
  2. Why Do We Need Decorators?
  3. Functions Are First-Class Objects
  4. Functions Returning Functions
  5. Creating Your First Decorator
  6. The @ Syntax Explained
  7. Decorators with Function Arguments
  8. Preserving Metadata with functools.wraps
  9. Decorators That Return Values
  10. Decorators with Parameters
  11. Real-World Examples
  12. Stacking Multiple Decorators
  13. Class-Based Decorators
  14. Built-in Python Decorators
  15. Decorators in FastAPI
  16. Common Mistakes
  17. Python Decorator Interview Questions
  18. FAQ
  19. Conclusion

What Is a Decorator?

Decorators are one of Python’s most powerful and elegant features. They allow developers to modify or extend the behavior of functions and methods without changing the original source code.

A decorator is simply a function that takes another function as input, adds some functionality, and returns a new function.

Think of a decorator as a wrapper around an existing function.

For example:

def greet():
    print("Hello World")

Suppose you want to log every time the function is called.

You could modify the function directly, but that quickly becomes repetitive when multiple functions need the same behavior.

Decorators solve this problem by allowing reusable behavior to be applied externally.


Why Do We Need Decorators?

In software engineering, certain functionality appears repeatedly across different parts of an application:

  • Logging
  • Authentication
  • Authorization
  • Performance monitoring
  • Input validation
  • Caching
  • Rate limiting
  • Error handling

Without decorators, this logic would be duplicated in many places.

Decorators help implement the DRY (Don’t Repeat Yourself) principle.

For example, instead of writing:

def process_order():
    print("Logging started")
    print("Processing order")

for every function, we can write a reusable decorator once.


Functions Are First-Class Objects

To understand decorators, you first need to understand that functions in Python are first-class objects.

This means functions can:

  • Be assigned to variables
  • Be passed as arguments
  • Be returned from functions
  • Be stored in data structures

Example:

def greet():
    print("Hello")

say_hello = greet

say_hello()

Output:

Hello

The variable say_hello points to the same function object as greet.

This flexibility is the foundation of decorators.


Functions Returning Functions

Python functions can create and return other functions.

def outer():

    def inner():
        print("Inside inner function")

    return inner

func = outer()
func()

Output:

Inside inner function

This behavior is known as a closure.

Closures are one of the building blocks of decorators.


Creating Your First Decorator

Let’s create a simple logging decorator.

def logger(func):

    def wrapper():
        print("Function started")

        func()

        print("Function finished")

    return wrapper

Apply it manually:

def greet():
    print("Hello World")

greet = logger(greet)

greet()

Output:

Function started
Hello World
Function finished

Notice that the original function remains untouched.


The @ Syntax Explained

Python provides cleaner syntax for applying decorators.

Instead of:

greet = logger(greet)

we can write:

@logger
def greet():
    print("Hello World")

Python automatically converts this to:

def greet():
    print("Hello World")

greet = logger(greet)

The @ symbol is simply syntactic sugar.


How Decorators Work Internally

Consider:

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

Execution flow:

  1. Python creates greet()
  2. Python passes greet into logger()
  3. logger() returns wrapper()
  4. greet now points to wrapper()
  5. Calling greet() executes wrapper()

Visual representation:

greet()
   |
   v
wrapper()
   |
   v
original greet()

Decorators with Function Arguments

A common beginner mistake:

def logger(func):

    def wrapper():
        return func()

    return wrapper

Works:

@logger
def hello():
    print("Hello")

Fails:

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

Error:

TypeError: wrapper() takes 0 positional arguments

Solution: Use *args and **kwargs

def logger(func):

    def wrapper(*args, **kwargs):

        print("Function started")

        result = func(*args, **kwargs)

        print("Function finished")

        return result

    return wrapper

Now the decorator works with any function signature.


Preserving Metadata with functools.wraps

Consider:

@logger
def greet():
    """Greets the user"""

Checking metadata:

print(greet.__name__)

Output:

wrapper

The original metadata is lost.

The Solution

from functools import wraps

def logger(func):

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

    return wrapper

Now:

print(greet.__name__)

Output:

greet

Always use @wraps in production code.


Decorators That Return Values

A decorator should usually return the wrapped function’s result.

def logger(func):

    def wrapper(*args, **kwargs):

        print("Running function")

        result = func(*args, **kwargs)

        return result

    return wrapper

Example:

@logger
def square(n):
    return n * n

print(square(4))

Output:

Running function
16

Decorators with Parameters

Sometimes the decorator itself needs configuration.

Usage:

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

Implementation:

def repeat(times):

    def decorator(func):

        def wrapper(*args, **kwargs):

            for _ in range(times):
                func(*args, **kwargs)

        return wrapper

    return decorator

Output:

Hello
Hello
Hello

Notice the extra nesting layer.

This pattern is often called a decorator factory.


Real-World Examples

Example 1: Execution Timer

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:.4f}s")

        return result

    return wrapper

Usage:

@timer
def slow_task():

    time.sleep(2)

slow_task()

Output:

Execution time: 2.0001s

Example 2: Authentication Decorator

def login_required(func):

    def wrapper(user):

        if not user["logged_in"]:
            print("Access denied")
            return

        return func(user)

    return wrapper

Usage:

@login_required
def dashboard(user):
    print("Welcome")

dashboard({"logged_in": True})

Output:

Welcome

Example 3: Caching Results

def cache(func):

    storage = {}

    def wrapper(n):

        if n in storage:
            return storage[n]

        result = func(n)

        storage[n] = result

        return result

    return wrapper

Usage:

@cache
def square(n):
    print("Computing...")
    return n * n

First call:

Computing...

Second call:

(No computation)

The value is returned from cache.


Stacking Multiple Decorators

You can apply multiple decorators to a function.

@decorator1
@decorator2
def my_function():
    pass

Equivalent to:

my_function = decorator1(
                 decorator2(my_function)
             )

Execution order:

decorator2
decorator1
function

This is a common interview question.


Class-Based Decorators

Decorators can also be implemented using classes.

class Logger:

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

    def __call__(self, *args, **kwargs):

        print("Function called")

        return self.func(*args, **kwargs)

Usage:

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

Output:

Function called
Hello

Built-in Python Decorators

Python provides several useful decorators.

@staticmethod

class Math:

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

No object instance required.


@classmethod

class User:

    count = 0

    @classmethod
    def get_count(cls):
        return cls.count

Receives the class reference.


@property

class Employee:

    def __init__(self, salary):
        self._salary = salary

    @property
    def salary(self):
        return self._salary

Usage:

employee.salary

instead of:

employee.salary()

Decorators in FastAPI

FastAPI relies heavily on decorators.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def home():
    return {"message": "Hello World"}

The decorator registers the function as an HTTP endpoint.

When a request arrives at /, FastAPI executes the associated function.

This is one of the most practical decorator examples you’ll encounter in modern Python development.


Common Mistakes

Forgetting to Return the Result

Wrong:

def wrapper():
    func()

Correct:

def wrapper():
    return func()

Forgetting *args and **kwargs

Wrong:

def wrapper():
    pass

Correct:

def wrapper(*args, **kwargs):
    pass

Forgetting functools.wraps

Wrong:

def wrapper(*args, **kwargs):
    pass

Correct:

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

Modifying Global State Unnecessarily

Decorators should ideally be predictable and free from hidden side effects.


Python Decorator Interview Questions

1. What is a decorator?

A decorator is a function that modifies or extends another function’s behavior without changing its source code.


2. Why are decorators useful?

They help implement reusable functionality such as:

  • Logging
  • Authentication
  • Validation
  • Caching
  • Monitoring

3. What enables decorators in Python?

Functions being first-class objects.


4. What does the @ syntax do?

It converts:

@decorator
def func():
    pass

into:

func = decorator(func)

5. Why use functools.wraps?

To preserve:

  • Function name
  • Documentation
  • Metadata
  • Signature information

6. What is the difference between a closure and a decorator?

A closure remembers variables from an outer scope.

A decorator is a practical application of closures used to modify functions.


7. Can decorators accept parameters?

Yes.

Example:

@retry(3)
def api_call():
    pass

8. What is the order of execution for stacked decorators?

@A
@B
def func():

becomes:

func = A(B(func))

9. Can classes be used as decorators?

Yes, by implementing the __call__() method.


10. Where are decorators commonly used?

  • FastAPI
  • Flask
  • Django
  • Logging systems
  • Monitoring tools
  • Authentication frameworks

FAQ

Are decorators difficult to learn?

Initially yes, because they combine multiple Python concepts such as functions, closures, and higher-order programming. Once these concepts are understood, decorators become much easier.

Do decorators affect performance?

Decorators introduce a very small overhead because an additional function call is executed. In most applications, this overhead is negligible.

Can a decorator modify arguments?

Yes. A decorator can inspect, validate, or even modify arguments before calling the original function.

Are decorators object-oriented?

No. Decorators are primarily a functional programming feature, although class-based decorators also exist.

Can multiple decorators be applied?

Yes. Decorators can be stacked and are executed from the innermost decorator outward.


Conclusion

Decorators are one of Python’s most elegant and practical features. They allow developers to add functionality to existing code without modifying the original implementation.

By understanding decorators, you’ll gain a deeper understanding of:

  • Higher-order functions
  • Closures
  • Function metadata
  • Framework internals
  • Advanced Python design patterns

Whether you’re preparing for Python interviews, building APIs with FastAPI, or developing production-grade applications, decorators are a skill you will use repeatedly throughout your Python journey.

If you’re serious about becoming a professional Python developer, mastering decorators is not optional—it’s essential.

Leave a Comment