Wrapping functions & functools.wraps

Decorators

Lesson 4 of 20 · 12 min · Module 1: Functions, deeper

Logging, timing, caching, and access checks often apply to many functions. A decorator adds such behaviour without changing the function’s own code.

Start this lesson →

Free. No installation: you write and run the code in your browser, and it is checked automatically.

By the end, you’ll be able to…

  • Explain what a decorator does
  • Write a wrapper that forwards any arguments
  • Preserve function metadata with functools.wraps

The idea

A decorator is a function that takes a function and returns a new one, usually a wrapper that calls the original. @count_calls above a def is shorthand for f = count_calls(f). Use functools.wraps so the wrapper keeps the original name and docstring.

import functools

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

@shout
def greet(name):
    return "hi " + name

print(greet("sam"))

01The @ syntax is shorthand

@count_calls on top of def square(x) runs square = count_calls(square). After that, the name square refers to the wrapper.

02Forward everything

wrapper(*args, **kwargs) accepts any call, and func(*args, **kwargs) passes it on unchanged. That makes one decorator usable for every function.

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

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

print(add(2, 3))
Output
calling add
5

03Functions can have attributes

A function is an object, so wrapper.calls = 0 stores data on it. functools.wraps copies the original __name__ and __doc__ to the wrapper, which keeps error messages and documentation tools accurate.

Your turn

Write a decorator count_calls. The wrapped function must return exactly what the original returns, accept any arguments, keep its __name__, and expose a calls attribute that counts how many times it has been called.

What the checks look for

  • Counts calls
  • Returns the original result and passes keyword arguments
  • Starts at zero and counts separately per function
  • Keeps the original function name
Try it in the editor →

Take this with you

A decorator wraps behaviour around a function; wraps keeps its identity.

Go a little further

Write a decorator retry(times) that takes an argument and retries a function that raises an exception.

Learn Python Intermediate for free

20 short lessons with real code and instant checks.

See the whole course →