Skip to content

Decorators in Python

A decorator is a function that takes another function, adds some extra work before or after it, and gives back a new function, without changing the original function's code.

Think of it like a gift wrapper: the gift (your function) stays the same, the wrapper (decorator) adds something around it.

Decorators are used everywhere in real projects: logging, timing, login checks, caching and validation. Frameworks such as FastAPI and Flask use them for routes, for example @app.get("/").

Setup

import time
from functools import wraps

Step 1: Functions are objects

A function can be stored in a variable, just like a number or string.

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

greet = say_hello        # no brackets -> we are NOT calling it
print("Step 1:")
greet()                  # now we call it -> prints Hello!

Step 2: Pass a function to another function

def call_twice(func):
    func()
    func()

print("\nStep 2:")
call_twice(say_hello)

Step 3: Function inside a function

def outer():
    def inner():
        print("I am inner function")
    return inner         # return the function itself (not inner())

print("\nStep 3:")
my_func = outer()
my_func()

Step 4: Our first decorator (manual way)

def my_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

def say_hi():
    print("Hi Students")

print("\nStep 4:")
say_hi = my_decorator(say_hi)   # wrap say_hi
say_hi()

Step 5: the @ symbol (short way)

@my_decorator is the same as say_bye = my_decorator(say_bye)

@my_decorator
def say_bye():
    print("Bye Students")

print("\nStep 5:")
say_bye()

Step 6: Decorator for functions with arguments

*args and **kwargs accept any number of arguments.

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

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

print("\nStep 6:")
result = add(10, 20)
print("Result:", result)

Step 7: Returning a value from the decorated function

Always "return func(...)" inside wrapper, otherwise the result is lost.

def double_result(func):
    def wrapper(*args, **kwargs):
        value = func(*args, **kwargs)
        return value * 2
    return wrapper

@double_result
def get_number(n):
    return n

print("\nStep 7:")
print("get_number(5) ->", get_number(5))    # 10

Step 8: Real example - timer decorator

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

@timer
def slow_task():
    time.sleep(1)
    print("Task finished")

print("\nStep 8:")
slow_task()

Step 9: Real example - login check

def login_required(func):
    def wrapper(user):
        if user.get("logged_in"):
            return func(user)
        else:
            print("Access denied! Please login first.")
    return wrapper

@login_required
def view_profile(user):
    print("Welcome", user["name"])

print("\nStep 9:")
view_profile({"name": "Sudheer", "logged_in": True})
view_profile({"name": "Guest", "logged_in": False})

Step 10: Decorator with its own argument

Need one more outer function to receive the argument.

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

@repeat(3)
def cheer(name):
    print("Well done,", name)

print("\nStep 10:")
cheer("Divyani")

Step 11: Using more than one decorator

The decorator closest to the function runs first (bottom to top).

def bold(func):
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold
@italic
def text():
    return "Python"

print("\nStep 11:")
print(text())     # <b><i>Python</i></b>

Step 12: functools.wraps (good practice)

Without @wraps, the function name becomes "wrapper".

def no_wraps(func):
    def wrapper():
        return func()
    return wrapper

def with_wraps(func):
    @wraps(func)              # keeps original name and docstring
    def wrapper():
        return func()
    return wrapper

@no_wraps
def first():
    """This is first function"""

@with_wraps
def second():
    """This is second function"""

print("\nStep 12:")
print("Without wraps:", first.__name__)    # wrapper
print("With wraps   :", second.__name__)   # second

Summary

  • A decorator takes a function and returns a new function.
  • @decorator_name above a function = func = decorator_name(func)
  • Use *args, **kwargs in wrapper so it works with any function.
  • Use "return func(...)" so the result is not lost.
  • Use @wraps(func) to keep the original function's name.
  • Common uses: logging, timing, login checks, caching, validation.