Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 13 min read 👁 2 views 📅 Oct 22, 2026

Decorators: Adding Features to Functions

Learn how Python decorators wrap extra features around a function, like gift paper around a gift. Write your own timing, logging, login check and retry decorators, and meet @property, @staticmethod and @classmethod.

The Gift-Wrapping Counter

During the Diwali season, many shops in a mall have a gift-wrapping counter. You buy a watch, a book or a box of sweets, and the person at the counter adds coloured paper, a ribbon and a small card. The gift inside does not change at all. It is the same watch. But now it has something extra around it, and the same counter can wrap any gift you bring.

A decorator in Python does exactly this for functions. It takes a function, wraps extra behaviour around it, and gives you back a function that still does the original job, plus something more: printing a log, measuring time, checking a login, or trying again after a failure. You write that extra behaviour once and use it on as many functions as you like.

You will meet decorators everywhere in real Python code: in web frameworks like Flask and Django, in testing tools, and in classes. Once you understand how they work, that code stops looking like magic.

Step 1: Functions Are Objects

Decorators depend on one idea: in Python, a function is an object, just like a number or a string. That means you can store it in a variable, pass it to another function, and create and return it from inside another function. Notice the difference between pack_watch (the function itself) and pack_watch() (calling it).

def pack_watch():
    print("Packing a wrist watch")

# 1. A function can be stored in a variable (no brackets, so it is not called)
task = pack_watch
task()                       # Packing a wrist watch

# 2. A function can be passed to another function
def do_twice(func):
    func()
    func()

do_twice(pack_watch)         # Packing a wrist watch (two times)

# 3. A function can be created inside another function and returned
def make_greeting(shop_name):
    def greet(customer):
        # greet remembers shop_name: this is called a closure
        print(f"Welcome to {shop_name}, {customer}!")
    return greet

greet_customer = make_greeting("Lakshmi Gift Corner")
greet_customer("Anita")      # Welcome to Lakshmi Gift Corner, Anita!

The last part builds on what you learned about scope in the More on Functions lesson. The inner function greet uses shop_name from the outer function, and it still remembers that value after make_greeting has finished. A function that remembers values from the place where it was created is called a closure. Every decorator you write will use a closure.

Step 2: Your First Decorator

A decorator is simply a function that takes a function and returns a new function. Here, gift_wrap receives func, builds an inner function called wrapper that prints something, calls func, and prints again. Then it returns wrapper.

def gift_wrap(func):
    def wrapper():
        print("Adding wrapping paper...")
        func()                          # the original gift, unchanged
        print("Tying the ribbon. Ready!")
    return wrapper

def pack_book():
    print("Packing a story book")

pack_book = gift_wrap(pack_book)        # replace it with the wrapped version
pack_book()

# Adding wrapping paper...
# Packing a story book
# Tying the ribbon. Ready!

The line pack_book = gift_wrap(pack_book) is the key. The name pack_book now points to wrapper. Because wrapper is a closure, it remembers the original pack_book as func and calls it in the middle.

Step 3: The @ Syntax and Passing Arguments Through

Writing name = decorator(name) after every function is tiring, so Python gives a shortcut. Put @gift_wrap on the line just above def, and Python does the replacement for you. The two forms are exactly the same.

Our first wrapper took no arguments, so it could only wrap functions with no arguments. A useful decorator must work for any function. The fix is *args and **kwargs, which you met in the More on Functions lesson: wrapper collects whatever positional and keyword arguments it receives and passes all of them on to func. It must also return the result of func, or the caller gets None.

def gift_wrap(func):
    def wrapper(*args, **kwargs):
        print("Adding wrapping paper...")
        result = func(*args, **kwargs)  # pass every argument through
        print("Tying the ribbon. Ready!")
        return result                   # give back the original result
    return wrapper

@gift_wrap
def pack_gift(item, quantity=1):
    print(f"Packing {quantity} x {item}")
    return quantity * 50                # wrapping charge in rupees

charge = pack_gift("photo frame", quantity=2)
print("Wrapping charge: Rs.", charge)

# Adding wrapping paper...
# Packing 2 x photo frame
# Tying the ribbon. Ready!
# Wrapping charge: Rs. 100

This is the pattern you will use almost every time: accept *args, **kwargs, do something before, call func(*args, **kwargs), do something after, return the result.

Step 4: Keep the Function’s Name with functools.wraps

There is one small problem. After decoration, the function’s name and docstring belong to wrapper, not to your function. This confuses error messages, help text and testing tools. The standard library has a fix: put @functools.wraps(func) above your wrapper. It copies the name, the docstring and other details from the original function onto the wrapper.

import functools

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

def careful_wrap(func):
    @functools.wraps(func)              # copy the name and docstring
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain_wrap
def pack_toy():
    """Pack a soft toy in a box."""

@careful_wrap
def pack_pen():
    """Pack a pen in a gift case."""

print(pack_toy.__name__, "|", pack_toy.__doc__)   # wrapper | None
print(pack_pen.__name__, "|", pack_pen.__doc__)   # pack_pen | Pack a pen in a gift case.
Make it a habit. Use @functools.wraps(func) in every decorator you write. It costs one line and saves you confusing bugs later.

Step 5: Practical Decorators

Timing a function

Which version of a function is faster? A timing decorator answers this without changing the function. time.perf_counter() is a clock made for measuring short durations. You read it before and after the call and subtract.

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        seconds = time.perf_counter() - start
        print(f"{func.__name__} took {seconds:.4f} seconds")
        return result
    return wrapper

@timer
def total_sales(bills):
    return sum(bills)

@timer
def slow_total(bills):
    total = 0
    for bill in bills:
        total = total + bill
    return total

bills = list(range(1, 2_000_001))
print(total_sales(bills))   # total_sales took ... seconds, then 2000001000000
print(slow_total(bills))    # slow_total took ... seconds (usually longer), then 2000001000000

Your times will be different from anyone else’s, because they depend on the computer. The built-in sum() is usually much faster than a hand-written loop.

Decorators with their own arguments: retry

Sometimes the decorator itself needs a setting, for example “try 3 times”. Then you need one more layer: a function that takes the setting and returns a decorator. This outer function is often called a decorator factory. When Python sees @retry(times=3), it first calls retry(times=3), gets a decorator back, and then applies that decorator to your function.

Here, sending a delivery SMS fails twice because of a pretend network problem, and the decorator quietly tries again:

import functools

def retry(times):
    """A decorator factory: retry(3) builds a decorator that tries 3 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 error:
                    print(f"Attempt {attempt} failed: {error}")
            raise ConnectionError(f"Gave up after {times} attempts")
        return wrapper
    return decorator

calls = {"count": 0}

@retry(times=3)
def send_delivery_sms(order_id):
    calls["count"] += 1
    if calls["count"] < 3:              # pretend the network fails twice
        raise ConnectionError("network busy")
    return f"SMS sent for order {order_id}"

print(send_delivery_sms(4521))

# Attempt 1 failed: network busy
# Attempt 2 failed: network busy
# SMS sent for order 4521

Three levels of functions can look confusing at first. Read it from the outside in: retry holds the setting, decorator receives the function, and wrapper runs every time the function is called. In real code you would also wait a little between attempts with time.sleep().

Built-in Decorators You Will Meet

Python already includes some decorators for classes. You saw classes in the Classes and Objects lesson. These three are the most common:

DecoratorWhat it does
@propertyLets you read a method like an attribute: order.total instead of order.total(). Good for values that are calculated from other attributes.
@staticmethodA method that needs neither self nor the class. It lives inside the class only because it belongs there logically.
@classmethodReceives the class as cls instead of an object. Often used for other ways to create objects, such as from text.
class GiftOrder:
    gst_rate = 0.18
    orders_today = 0

    def __init__(self, item, price):
        self.item = item
        self.price = price
        GiftOrder.orders_today += 1

    @property
    def total(self):
        """Used like an attribute: order.total, with no brackets."""
        return round(self.price * (1 + self.gst_rate), 2)

    @staticmethod
    def is_valid_message(message):
        """A helper that needs neither self nor the class."""
        return 0 < len(message) <= 50

    @classmethod
    def from_text(cls, text):
        """Another way to create an order, from text like 'Mug,250'."""
        item, price = text.split(",")
        return cls(item, float(price))

order = GiftOrder.from_text("Coffee mug,250")
print(order.item, order.total)                         # Coffee mug 295.0
print(GiftOrder.is_valid_message("Happy Diwali!"))     # True
print(GiftOrder.orders_today)                          # 1

Real-Life Example: The Gift Shop Order System

A gift shop’s billing program has many functions: placing orders, cancelling orders, applying discounts. Every one of them should be logged, and only logged-in staff may use them. Instead of copying the same checks into every function, we write two decorators and stack them. The program uses a simple dictionary called current_user to pretend someone is logged in.

import functools
from datetime import datetime

current_user = {"name": "Kavya", "logged_in": True, "role": "staff"}

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        time_now = datetime.now().strftime("%H:%M")
        print(f"[{time_now}] {func.__name__} called with {args}")
        result = func(*args, **kwargs)
        print(f"[{time_now}] {func.__name__} returned {result!r}")
        return result
    return wrapper

def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not current_user["logged_in"]:
            print("Please log in first.")
            return None
        return func(*args, **kwargs)
    return wrapper

@log_call
@login_required
def place_order(item, price, wrap=True):
    total = price + (50 if wrap else 0)
    return f"{item} ordered by {current_user['name']}, total Rs. {total}"

place_order("Silk scarf", 799)

current_user["logged_in"] = False
place_order("Brass diya", 349, wrap=False)

# [10:30] place_order called with ('Silk scarf', 799)
# [10:30] place_order returned 'Silk scarf ordered by Kavya, total Rs. 849'
# [10:30] place_order called with ('Brass diya', 349)
# Please log in first.
# [10:30] place_order returned None

When you stack decorators, the one closest to the function is applied first. So login_required wraps place_order, and log_call wraps that result. When you call place_order, the outer layer (logging) runs first, then the login check, then your function. Your times in the output will match your own clock.

Try these changes:

  1. Swap the order of the two decorators and run it again. What does the log show when the user is not logged in?
  2. Add a cancel_order(order_id) function with both decorators.
  3. Add the timer decorator from Step 5 on top as well.

Common Mistakes

  • Forgetting to return the result. If wrapper calls func but does not return its value, every decorated function returns None.
def gift_wrap(func):
    def wrapper(*args, **kwargs):
        print("Wrapping...")
        func(*args, **kwargs)           # mistake: the result is thrown away
    return wrapper

@gift_wrap
def price_of(item):
    return 199

print(price_of("keychain"))   # Wrapping... then None, not 199
# Fix: write  return func(*args, **kwargs)  inside wrapper
  • Calling the function instead of passing it. Inside a decorator, return wrapper is correct. return wrapper() runs the wrapper once, at decoration time, and returns its result instead of a function.
  • Leaving out *args, **kwargs. A wrapper() with no parameters fails with a TypeError as soon as the decorated function is called with an argument.
  • Mixing up @retry and @retry(3). A decorator factory must be called with brackets. Writing @retry alone passes your function in as times, and your function silently stops working the way you expect.
  • Skipping functools.wraps. Everything seems to work, but every function is now named wrapper, which makes errors and logs hard to read.

Practice Task: Two Decorators for the Shop

Write two decorators for the gift shop and use the test code at the bottom of the starter to check them. Your solution should:

  • Have a count_calls decorator that prints check_stock call number 1, then 2, and so on.
  • Have an admin_only decorator that prints Admins only. and returns None unless current_user["role"] is "admin".
  • Pass all arguments through with *args, **kwargs and return the original result.
  • Use functools.wraps in both.
import functools

current_user = {"name": "Kavya", "role": "staff"}

def count_calls(func):
    # TODO 1: count how many times func is called and print
    #         "<name> call number <n>" before calling it
    #         Hint: store the count on the wrapper, like wrapper.calls = 0
    # TODO 3: use functools.wraps
    return func   # replace this with your wrapper

def admin_only(func):
    # TODO 2: run func only if current_user["role"] == "admin",
    #         otherwise print "Admins only." and return None
    # TODO 3: use functools.wraps
    return func   # replace this with your wrapper

@count_calls
def check_stock(item):
    return f"{item}: 12 in stock"

@admin_only
def change_price(item, new_price):
    return f"{item} now costs Rs. {new_price}"

print(check_stock("Teddy bear"))
print(check_stock("Photo frame"))
print(change_price("Teddy bear", 499))
current_user["role"] = "admin"
print(change_price("Teddy bear", 499))
Show one possible answer (try it yourself first!)
import functools

current_user = {"name": "Kavya", "role": "staff"}

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        print(f"{func.__name__} call number {wrapper.calls}")
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

def admin_only(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if current_user["role"] != "admin":
            print("Admins only.")
            return None
        return func(*args, **kwargs)
    return wrapper

@count_calls
def check_stock(item):
    return f"{item}: 12 in stock"

@admin_only
def change_price(item, new_price):
    return f"{item} now costs Rs. {new_price}"

print(check_stock("Teddy bear"))
print(check_stock("Photo frame"))
print(change_price("Teddy bear", 499))
current_user["role"] = "admin"
print(change_price("Teddy bear", 499))

# check_stock call number 1
# Teddy bear: 12 in stock
# check_stock call number 2
# Photo frame: 12 in stock
# Admins only.
# None
# Teddy bear now costs Rs. 499

Key Takeaways

  • A decorator is a function that takes a function and returns a new function with extra behaviour, like wrapping paper around a gift.
  • @my_decorator above a function is a shortcut for func = my_decorator(func).
  • Use *args, **kwargs in the wrapper, call the original function and return its result.
  • Always add @functools.wraps(func) to keep the original name and docstring.
  • A decorator with settings, like @retry(times=3), needs one more outer function that returns the decorator.
  • @property, @staticmethod and @classmethod are built-in decorators for classes.

❓ Frequently Asked Questions

No. The original function stays exactly the same. The decorator only puts a new function around it, and the name now points to that new function.
The decorator function itself runs once, when Python reads the def line. The wrapper runs every time you call the decorated function.
Yes, you can stack them on separate lines. The decorator closest to the def is applied first, and the top one runs first when the function is called.
Web frameworks use them to connect URLs to functions and to protect pages behind a login. Testing tools, caching with functools.lru_cache and class tools like @property also use them.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋