Python decorators: functions wrapping functions

Beginner Data Science Praktikum
Created by Pavel · 03.04.2026 at 12:13 UTC

Every time you write @dataclass above a class or @field_validator('name') above a method, you are using a decorator — and the @ is not magic. It is syntactic sugar for passing a function (or class) through another callable that wraps or replaces it.

@shout above def greet(name) is exactly equivalent to writing greet = shout(greet) after the definition. The decorator shout receives the original function, and whatever it returns is what the name greet now refers to. The standard pattern: an outer function receives fn, defines an inner wrapper(*args, **kwargs) that does something before or after calling fn, and returns wrapper. The returned wrapper replaces the original function.

The most common bug: forgetting return wrapper from the outer function. Without it, the decorated name becomes None, and calling it raises TypeError: 'NoneType' object is not callable. It is a silent, confusing failure because the decorator runs fine — it just returns nothing.

Use functools.wraps(fn) on the wrapper to copy the original function's __name__, __doc__, and other metadata. Without it, help(greet) shows wrapper instead of greet, and debuggers show the wrong name in tracebacks. This is a small detail that matters in production.

These concepts connect directly to what you have already used: @dataclass is a class decorator that rewrites the class body to inject __init__, __repr__, etc. @classmethod is a method decorator. @field_validator in Pydantic registers your method as a validator on the model schema.

Functools docs: [1], Python glossary on decorators: [2].


Sources

University approvals: 0
Tasks
Question 1

What does this code print?

def shout(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    return f'hello {name}'

print(greet('Alice'))
Hint

greet is now shout(greet), so calling greet('Alice') calls wrapper('Alice').

Question 2

What does this code print?

def broken(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    # missing: return wrapper

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

print(add)
Hint

What does a Python function return if there is no return statement?

Question 3

What does this code print?

from functools import wraps

def traced(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@traced
def multiply(a, b):
    """Multiplies two numbers."""
    return a * b

print(multiply.__name__, multiply.__doc__)
Hint

@wraps(fn) copies the original function's name and doc to the wrapper.

Question 4

Implement a decorator timing(fn) that wraps a function, prints nothing, but adds an attribute call_count to the wrapper that starts at 0 and increments by 1 each time the wrapper is called. Return the original result.

Then implement test_timing() -> int that decorates a simple function, calls it 3 times, and returns the call count.

Submit both; tests call test_timing().

Hint

Python functions are objects — you can set wrapper.call_count = 0 after defining wrapper, then increment inside.

Starter code is prefilled; replace TODO blocks with your solution.
1 test case will be used for grading
Run checks runtime behavior only. Final correctness is evaluated when you submit.
Card Info
  • Topic: Data Science Praktikum
  • Difficulty: Beginner
  • Completed: 0 users
Creator
Pavel
Pavel