Python Tutorial menu

Python Decorators

A decorator wraps a function to add extra behaviour, without changing the function's own code.

Functions Are Values

In Python a function is an object, just like a number or a string. You can store it in a variable and pass it to another function.

Example

python
def shout(text):
    return text.upper() + "!"

speak = shout
print(speak("hello"))

def run_twice(func, value):
    return func(func(value))

print(run_twice(shout, "hi"))
Output
HELLO!
HI!!

Notice speak = shout has no brackets. We copy the function itself, we do not call it.

A Function Inside a Function

A function can create another function and return it.

Example

python
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply

eid_sale = make_discount(20)
print(eid_sale(5000))
print(eid_sale(1200))
Output
4000.0
960.0

The inner function apply remembers percent even after make_discount has finished. This is called a closure, and it is the key idea behind decorators.

Your First Decorator

A decorator is a function that takes a function and returns a new, wrapped function.

Example

python
def announce(func):
    def wrapper():
        print("--- start ---")
        func()
        print("--- end ---")
    return wrapper

@announce
def greet():
    print("Assalam o Alaikum!")

greet()
Output
--- start ---
Assalam o Alaikum!
--- end ---

The line @announce above def greet() is short for writing greet = announce(greet) after the function. From now on, calling greet() really calls wrapper().

Decorating Any Function

Most functions take arguments. Use *args and **kwargs in the wrapper so it accepts anything, and return the result of the original function.

Example

python
import functools

def log_call(func):
    @functools.wraps(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 bill(price, qty, tax=0):
    return price * qty + tax

bill(250, 4, tax=180)
print(bill.__name__)
Output
Calling bill with (250, 4) {'tax': 180}
bill returned 1180
bill

Always add @functools.wraps(func) to your wrapper. Without it, the decorated function loses its real name and docstring, which makes errors and help text confusing.

A Useful Example: Timing

Decorators are perfect for jobs you want on many functions: logging, timing, checking permissions, retrying.

Example

python
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 about {seconds:.1f} s")
        return result
    return wrapper

@timer
def slow_report():
    time.sleep(0.5)
    return "Report ready"

print(slow_report())
Output
slow_report took about 0.5 s
Report ready

Decorators With Arguments

To pass settings to a decorator, add one more outer function.

Example

python
import functools

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

@repeat(3)
def cheer(name):
    print(f"Well done, {name}!")

cheer("Ayesha")
Output
Well done, Ayesha!
Well done, Ayesha!
Well done, Ayesha!

Built-in Decorators You Will Meet

DecoratorWhat it does
@propertyLets a method be read like an attribute
@staticmethodA method that needs no self
@classmethodA method that gets the class (cls) instead of the object
@functools.cacheRemembers results so repeated calls are instant

Example

python
import functools

class Product:
    def __init__(self, price):
        self.price = price

    @property
    def price_with_tax(self):
        return round(self.price * 1.18)

p = Product(1000)
print(p.price_with_tax)

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(80))
Output
1180
23416728348467685

p.price_with_tax has no brackets because @property turns the method into a read-only attribute. Without @cache, fib(80) would take years. With it, the answer comes at once.

Web frameworks use decorators a lot. In Flask, for example, @app.route("/") connects a function to a web address.

Exercise

Exercise

What is @announce above def greet() short for?

Learn Python with a teacher

Join the Python Programming course at Workownix, online or in Orangi Town, Karachi, and get a certificate.

See the course Ask on WhatsApp