Skip to content

Expert · Advanced Python · topic 41 of 50 · 35 min

Decorators

Explanation

A decorator is a function that takes another function and returns a new function with extra behaviour, such as timing, logging, caching or a login check. You apply it by writing @decorator_name on the line above a def. Decorators work because Python functions are objects that you can pass around and return.

Functions are objects

In Python a function is a value, just like a number or a list. You can store it in a variable, put it in a list, pass it to another function, and return it from a function. Note the difference: greet is the function itself, greet() calls it.

python
def greet(name):
    return "Namaste, " + name

say = greet               # no brackets: we copy the function, we do not call it
print(say("Asha"))        # Namaste, Asha

def call_twice(func, value):
    return [func(value), func(value)]

print(call_twice(greet, "Ravi"))
A function passed into another function is often called a callback.

Inner functions and closures

You can define a function inside another function. The inner function can read the variables of the outer one. When the outer function returns the inner function, the inner function still remembers those variables. A function that remembers variables from where it was created is called a closure.

python
def make_gst_adder(rate):
    def add_gst(price):
        return round(price * (1 + rate / 100), 2)
    return add_gst

add_18 = make_gst_adder(18)
add_5 = make_gst_adder(5)
print(add_18(100))   # 118.0
print(add_5(100))    # 105.0
add_18 remembers rate = 18 even after make_gst_adder has finished.

Writing your first decorator

A decorator is a function that takes a function and returns a new function, usually an inner "wrapper" that does something before and after calling the original. The original function is not changed; it is wrapped.

Think of it like this: Think of gift wrapping. The gift inside stays the same, but the wrapping adds something: a ribbon, a card with your name. A decorator wraps a function in the same way. Calling the decorated function first meets the wrapping, which then opens it and runs the real function inside.
python
def announce(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        result = func(*args, **kwargs)
        print("Finished", func.__name__)
        return result
    return wrapper

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

add = announce(add)      # wrap it by hand
print(add(2, 3))
*args and **kwargs let the wrapper accept any arguments and pass them on unchanged.

Writing add = announce(add) after every function is untidy, so Python gives a short form. Put @announce on the line above the def. It means exactly the same thing: define the function, then replace it with announce(function).

python
def announce(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@announce
def multiply(a, b):
    return a * b

print(multiply(4, 5))
@announce above the def is the same as multiply = announce(multiply).

After wrapping, the function name becomes "wrapper" and its docstring (the help text in triple quotes) is lost. That confuses error messages, help() and testing tools. The fix is one line: decorate your wrapper with @functools.wraps(func), which copies the name, docstring and other details from the original.

python
import functools

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

@announce
def refund(amount):
    """Send money back to the customer."""
    return amount

print(refund.__name__)   # refund (without wraps it would say wrapper)
print(refund.__doc__)
Always use functools.wraps in real decorators.

Sometimes you want to configure a decorator, as in @repeat(3). Then you need one more layer: repeat(3) is called first and returns the real decorator. It looks complicated, so read it as "a function that makes a decorator".

python
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(3)
def roll():
    return "dice rolled"

print(roll())
Three layers: repeat(times) -> decorator(func) -> wrapper(*args).
UseWhat the decorator does
TimingNotes the time before and after, and reports how long the call took
LoggingWrites the function name and arguments to a log for debugging
CachingRemembers results so a repeated call with the same arguments is instant
Login checkIn web apps like Flask or Django, refuses the request if the user is not logged in
RetryCalls a flaky network function again a few times before giving up
python
import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(80))             # instant, thanks to the cache
print(fib.cache_info())
functools.lru_cache is a ready-made caching decorator from the standard library. LRU means "least recently used": when the cache is full, the oldest unused result is thrown away.
Tip: Only cache functions whose result depends on nothing but their arguments (pure functions). Caching a function that reads the current time or a database gives stale, wrong answers.
Careful: Do not write @announce() with brackets for a decorator that takes no arguments. Python would call announce() with no function and you get a TypeError. Brackets belong only to decorators that take arguments, like @repeat(3).

Watch the lesson

Decorators · 1/7

Decorators

This lesson is about decorators, the @ lines you see above functions in Flask, Django and many libraries.

Prefer a video by a teacher? Find free videos on “python decorators explained functools wraps” on YouTube ↗

In real life: A login check on a food delivery app

A food delivery backend has many functions that must only run for a logged-in user: view orders, change address, apply a coupon. Instead of copying the same "is the user logged in?" check into each one, the team writes one decorator and puts it on top of every protected function. Web frameworks such as Flask and Django ship decorators like this.

python
import functools

def login_required(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("logged_in"):
            return "Please log in first"
        return func(user, *args, **kwargs)
    return wrapper

@login_required
def my_orders(user):
    return user["name"] + " has 2 orders"

print(my_orders({"name": "Neha", "logged_in": True}))
print(my_orders({"name": "Guest", "logged_in": False}))
One decorator protects every function it is placed on.

Practice (0/3 done)

Task 1 · Code · Python

Write a decorator shout(func). The wrapped function should return the original result in CAPITAL letters. Example: a function that returns "jai hind" should return "JAI HIND" after @shout.

Task 2 · Code · Python

Write a decorator count_calls(func) that counts how many times the decorated function is called. Store the count in an attribute wrapper.calls (start at 0). Use functools.wraps so the function keeps its name.

Task 3 · Code · Python

Write a decorator require_login(func) for a web app. The decorated function always gets a user dict as its first argument. If user.get("logged_in") is not True, raise PermissionError("login required") and do NOT run the function. Otherwise return the function's result.

Quick quiz

Question 1

1.What is @timer above def report(): ... the same as?

Question 2

2.What is a closure?

Question 3

3.Why should a decorator use functools.wraps?

Question 4

4.Which function is a good fit for @functools.lru_cache?

Question 5

5.Why does a wrapper usually take *args and **kwargs?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Iterators and generators