FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Python Decorators Explained: A Complete Guide with Practical Examples

Understand Python decorators from first principles. Learn how they work, how to write your own, decorators with arguments, class decorators, and real-world use cases.

Yusuf SeyitoğluMarch 11, 20262 views9 min read

Python Decorators Explained: A Complete Guide with Practical Examples

Decorators are one of Python's most elegant features β€” and one of the most confusing for developers who encounter them without a solid foundation. Once you understand the mechanics, they become a natural and powerful tool.

This guide builds your understanding step by step, from functions as first-class objects to real-world decorator patterns.

Functions Are First-Class Objects

Before understanding decorators, you need to understand that in Python, functions are objects. They can be assigned to variables, passed as arguments, and returned from other functions.

python
def greet(name): return f"Hello, {name}" say_hello = greet # assign to variable print(say_hello("Alice")) # Hello, Alice def apply(func, value): # pass function as argument return func(value) print(apply(greet, "Bob")) # Hello, Bob

This is the foundation everything else builds on.

Higher-Order Functions

A function that takes a function as input or returns a function as output is called a higher-order function:

python
def make_multiplier(factor): def multiply(number): return number * factor return multiply # returning a function double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15

multiply is a closure β€” it remembers the value of factor from its enclosing scope even after make_multiplier has returned.

What Is a Decorator?

A decorator is a function that takes a function, wraps it with additional behavior, and returns the wrapped version.

python
def my_decorator(func): def wrapper(*args, **kwargs): print("Before the function runs") result = func(*args, **kwargs) print("After the function runs") return result return wrapper def say_hello(name): print(f"Hello, {name}!") say_hello = my_decorator(say_hello) # manual decoration say_hello("Alice")

Output:

code
Before the function runs Hello, Alice! After the function runs

The @ Syntax

Python provides syntactic sugar for decoration. @my_decorator above a function definition is exactly equivalent to func = my_decorator(func):

python
@my_decorator def say_hello(name): print(f"Hello, {name}!") -- Equivalent to: -- say_hello = my_decorator(say_hello)

Cleaner and more readable. The decorator is declared right where the function is defined.

Preserving Function Metadata

Without extra work, decorating a function replaces its name and docstring with the wrapper's:

python
@my_decorator def say_hello(name): """Say hello to someone.""" print(f"Hello, {name}!") print(say_hello.__name__) # "wrapper" β€” wrong! print(say_hello.__doc__) # None β€” wrong!

Fix this with functools.wraps:

python
import functools def my_decorator(func): @functools.wraps(func) # preserves __name__, __doc__, etc. def wrapper(*args, **kwargs): print("Before") result = func(*args, **kwargs) print("After") return result return wrapper

Always use @functools.wraps in your decorators.

Real-World Decorator Examples

Timing a function

python
import functools import time def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"{func.__name__} took {end - start:.4f}s") return result return wrapper @timer def slow_function(): time.sleep(0.5) return "done" slow_function() -- slow_function took 0.5003s

Caching / Memoization

python
import functools def memoize(func): cache = {} @functools.wraps(func) def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper @memoize def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(50)) # fast β€” cached results reused

Python also ships @functools.lru_cache for this:

python
from functools import lru_cache @lru_cache(maxsize=None) def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2)

Retry logic

python
import functools import time def retry(max_attempts=3, delay=1.0): def decorator(func): @functools.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 e: last_error = e print(f"Attempt {attempt} failed: {e}") if attempt < max_attempts: time.sleep(delay) raise last_error return wrapper return decorator @retry(max_attempts=3, delay=2.0) def call_external_api(url): -- simulate a flaky network call import random if random.random() < 0.7: raise ConnectionError("Network timeout") return {"status": "ok"}

Access control

python
import functools def require_auth(func): @functools.wraps(func) def wrapper(request, *args, **kwargs): if not request.get("user"): raise PermissionError("Authentication required") return func(request, *args, **kwargs) return wrapper @require_auth def get_profile(request): return {"name": request["user"]["name"]}

This is the same pattern Flask and Django use for @login_required.

Decorators with Arguments

When your decorator needs parameters, you add another layer:

python
def repeat(n): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def say_hi(): print("Hi!") say_hi() -- Hi! -- Hi! -- Hi!

@repeat(3) calls repeat(3) first, which returns decorator, which then wraps say_hi.

Stacking Decorators

You can apply multiple decorators. They are applied bottom-up:

python
@timer @retry(max_attempts=3) @require_auth def fetch_data(request): pass -- Equivalent to: -- fetch_data = timer(retry(max_attempts=3)(require_auth(fetch_data)))

The innermost decorator (require_auth) wraps first, then retry, then timer is outermost.

Class Decorators

Classes can also act as decorators by implementing __call__:

python
class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.call_count = 0 def __call__(self, *args, **kwargs): self.call_count += 1 print(f"Call #{self.call_count} to {self.func.__name__}") return self.func(*args, **kwargs) @CountCalls def greet(name): print(f"Hello, {name}!") greet("Alice") # Call #1 to greet greet("Bob") # Call #2 to greet print(greet.call_count) # 2

Common Interview Questions

Q: What does @staticmethod and @classmethod do?

Both are built-in decorators. @staticmethod defines a method that does not receive self or cls β€” it is just a regular function namespaced inside the class. @classmethod receives cls (the class itself) instead of self, useful for factory methods.

Q: What is the difference between a decorator and a context manager?

A decorator wraps a function, adding behavior before and after every call. A context manager (with statement) manages a resource for a block of code. contextlib.contextmanager lets you write a context manager using a generator, which looks similar to a decorator but serves a different purpose.

Q: Can decorators be applied to classes?

Yes. A class decorator receives the class as an argument and returns a modified class or a replacement. @dataclass is a well-known example.

Practice Python on Froquiz

Decorators are a common Python interview topic at intermediate and advanced levels. Test your Python skills on Froquiz β€” covering decorators, generators, comprehensions, OOP, and more.

Summary

  • Functions are first-class objects β€” they can be passed and returned like any value
  • A decorator is a function that wraps another function to add behavior
  • Always use @functools.wraps to preserve the original function's metadata
  • Add a parameter layer (def repeat(n): def decorator(func):) for decorators with arguments
  • Decorators are applied bottom-up when stacked
  • Real-world uses: timing, caching, retry, logging, authentication, rate limiting

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • System Design Fundamentals: Scalability, Load Balancing, Caching and DatabasesMar 12
  • CSS Advanced Techniques: Custom Properties, Container Queries, Grid Masonry and Modern LayoutsMar 12
  • GraphQL Schema Design: Types, Resolvers, Mutations and Best PracticesMar 12
All Blogs