Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🟡 Advanced Python  ·  Lesson 21

Decorators in Python

In this lesson: Understand Python decorators step by step — functions as objects, wrappers, @syntax and real uses like timing. In Hindi & English.

What is a decorator?

A decorator is a function that takes another function and returns a new function with extra behaviour – without changing the original code. You write it once and attach it with a single line that starts with @.

Real projects use decorators everywhere: Flask uses @app.route to connect a URL to a function, Django uses @login_required to protect pages, and Python itself gives @property, @staticmethod and @functools.lru_cache.

Without decoratorWith decorator
Copy the same logging/timing code into 20 functionsWrite the logic once, add @log_call above each function
Change the rule → edit 20 placesChange the rule → edit one decorator
Business logic mixed with checksFunction stays clean; checks live outside

Step 1: functions are objects

Decorators work because in Python a function is a normal object. You can store it in a variable, pass it to another function and return it from a function.

Python – functions_as_objects.py
def greet(name):
    return f"Namaste, {name}!"

say = greet                 # no brackets: we store the function itself
print(say("Riya"))
print(type(greet))

def run_twice(func, value):  # a function that receives a function
    return func(value) + " " + func(value)

print(run_twice(greet, "Aman"))
Namaste, Riya! <class 'function'> Namaste, Aman! Namaste, Aman!
💡 greet vs greet()

greet is the function object; greet() runs it and gives its result. A decorator receives greet, not greet().

Step 2: a function inside a function (closure)

An inner function can use variables of the outer function even after the outer function has finished. This “remembering” is called a closure, and every decorator is built on it.

Python – closure.py
def make_gst_calculator(rate):
    def add_gst(amount):
        return round(amount * (1 + rate / 100), 2)
    return add_gst               # return the inner function

gst_5 = make_gst_calculator(5)    # sweets
gst_18 = make_gst_calculator(18)  # electronics

print(gst_5(200))
print(gst_18(200))
210.0 236.0

Step 3: your first decorator

Now combine the two ideas. log_call receives a function, defines a wrapper that prints a message and then calls the original, and returns the wrapper.

Python – first_decorator.py
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"→ calling {func.__name__} with {args} {kwargs}")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} returned {result}")
        return result
    return wrapper

@log_call
def total_bill(price, qty, discount=0):
    return price * qty - discount

total_bill(120, 3, discount=20)
→ calling total_bill with (120, 3) {'discount': 20} ← total_bill returned 340

The line @log_call is only a shortcut. Python actually runs total_bill = log_call(total_bill) right after defining the function.

  • *args, **kwargs let the wrapper accept any arguments, so one decorator fits every function.
  • Always return result from the wrapper – forgetting it makes every decorated function return None.

Step 4: keep the name and docstring with functools.wraps

A plain wrapper hides the original function’s name and help text. That breaks debugging, documentation and frameworks like Flask that use the function name. functools.wraps copies them across.

Python – wraps_demo.py
import functools

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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

@plain
def fees_a():
    """Return the monthly fee."""

@proper
def fees_b():
    """Return the monthly fee."""

print(fees_a.__name__, "|", fees_a.__doc__)
print(fees_b.__name__, "|", fees_b.__doc__)
wrapper | None fees_b | Return the monthly fee.
⚠️ Rule

Add @functools.wraps(func) to every wrapper you write. It costs one line and prevents confusing bugs later.

Real example 1: measure how long a function takes

Python – timer.py
import functools, time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        ms = (time.perf_counter() - start) * 1000
        print(f"{func.__name__} took {'under 50' if ms < 50 else 'over 50'} ms")
        return result
    return wrapper

@timer
def sum_of_squares(n):
    return sum(i * i for i in range(n))

print(sum_of_squares(1000))
sum_of_squares took under 50 ms 332833500

We print “under/over 50 ms” instead of the exact number so the output is stable on every computer; in your own code print f"{ms:.2f} ms".

Real example 2: a decorator that takes arguments

To pass settings such as the number of retries, add one more outer level: a function that receives the settings and returns the decorator.

Python – retry.py
import functools

def retry(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ConnectionError as err:
                    print(f"attempt {attempt} failed: {err}")
            raise ConnectionError(f"{func.__name__} failed {times} times")
        return wrapper
    return decorator

calls = {"n": 0}

@retry(times=3)
def send_sms(phone):
    calls["n"] += 1
    if calls["n"] < 3:                       # fail twice, then succeed
        raise ConnectionError("network busy")
    return f"SMS sent to {phone}"

print(send_sms("98290xxxxx"))
attempt 1 failed: network busy attempt 2 failed: network busy SMS sent to 98290xxxxx
LevelReceivesReturns
retry(times)settingsdecorator
decorator(func)the functionwrapper
wrapper(*args)the call argumentsthe result

Real example 3: allow only logged-in admins

Python – require_role.py
import functools

def require_role(role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                return f"403 Forbidden: {user['name']} is not {role}"
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_student(user, roll_no):
    return f"Student {roll_no} deleted by {user['name']}"

teacher = {"name": "Meena", "role": "teacher"}
admin = {"name": "Gagan", "role": "admin"}
print(delete_student(teacher, 21))
print(delete_student(admin, 21))
403 Forbidden: Meena is not admin Student 21 deleted by Gagan

This is exactly how @login_required in Django and @jwt_required in Flask work: the check runs before your function, so you never forget it.

Real example 4: built-in caching with lru_cache

Python – lru_cache.py
from functools import lru_cache

calls = 0

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

print(fib(35))
print("function calls:", calls)
print(fib.cache_info())
9227465 function calls: 36 CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)

Without the cache, fib(35) makes almost 30 million calls; with it, only 36. Use caching for pure functions whose result depends only on the arguments.

Stacking decorators: order matters

Python – stacking.py
import functools

def bold(func):
    @functools.wraps(func)
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold
@italic
def title():
    return "Result 2026"

print(title())   # same as bold(italic(title))
<b><i>Result 2026</i></b>

The decorator closest to the function is applied first. Read the stack from bottom to top.

Common mistakes

MistakeWhat happensFix
Forgetting return result in wrapperFunction returns NoneReturn the original result
Writing @timer() for a decorator without argumentsTypeError: missing funcUse @timer; brackets only for decorator factories
No functools.wrapsWrong name in logs, Flask “endpoint overwriting” errorsAlways add @functools.wraps(func)
Caching a function that reads time or a databaseStale, wrong answersCache only pure functions

Practice tasks

  1. Write @count_calls that prints how many times a function has been called.
  2. Write @validate_positive that raises ValueError if any number argument is negative.
  3. Write @cache_for(seconds) that remembers a result only for the given time (use time.time()).
  4. Apply @timer and @log_call together and explain the printed order.

Summary

  • A decorator takes a function and returns a new function with extra behaviour.
  • @name above a function means func = name(func).
  • Use *args, **kwargs, return the result and add functools.wraps.
  • Need settings? Add an outer function – a decorator factory.
  • Typical uses: logging, timing, retries, permissions, caching, routing.

Frequently Asked Questions

Are decorators only for functions?
No. They also work on methods and classes. @property, @classmethod and @staticmethod are decorators used inside classes, and a class decorator such as @dataclass receives a class and returns a modified class.
Do decorators make code slower?
Each call goes through one extra function call, which is a few hundred nanoseconds. That is negligible compared with database or network work. lru_cache can make code dramatically faster.
Is a decorator asked in interviews?
Yes, very often. Be ready to write a timing or logging decorator, explain closures and functools.wraps, and write a decorator that accepts arguments.
← Back to Python Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

💻 Live Code Editor

This page's programs are ready here — run them, edit them, and learn. No installation needed.
Powered by OneCompiler. The code loads into the editor automatically — press Run to see the output. If the editor does not open, open it in a new tab.
Next topicIterators and Generators Your progress: 21 / 66 (32%) · Lesson 22 / 66 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free