Function Decorators

RoadmapsPython

Overview

A decorator is a function that takes another function and extends its behavior without explicitly modifying it. The `@decorator` syntax is syntactic sugar for `func = decorator(func)`. Under the hood, a decorator returns a wrapper function (a closure) that replaces the original function. It acts like gift wrapping a box: the original box is still inside, but it now has a shiny exterior. When writing decorators, you must be careful because the wrapper replaces the original function entirely. This causes the function to lose its original metadata, such as its name and docstring. def simple_decorator(func): def wrapper(): return func() return wrapper @simple_decorator def greet(): pass print(greet.__name__) # Prints 'wrapper', not 'greet' This metadata loss can break debugging tools or web frameworks that rely on function names. This trap is fixed using `@wraps`, which is covered fully in functools-wraps.

Decorators allow you to cleanly extract reusable cross-cutting concerns—like logging, authentication, or timing—away from the core business logic of your functions.

Where used: FastAPI route definitions (`@app.get`), Flask routes (`@app.route`), Built-in class properties (`@property`)

Why learn this

Code walkthrough

def uppercase(func):
  def wrapper():
    original_result = func()
    return original_result.upper()
  return wrapper

@uppercase
def greet():
  return 'hello'

print(greet())

Focus: return original_result.upper()

Aha moment

def no_op(func):
  pass

@no_op
def greet():
  return 'hello'

print(greet())

Prediction: What does this code print?

Common guess: It prints `'hello'`, assuming the decorator does nothing.

It throws a `TypeError`. Since `no_op` doesn't explicitly return anything, it returns `None`. The decorator syntax does `greet = no_op(greet)`, so `greet` is now `None`.

Common mistakes

Glossary

syntactic sugar
Syntax in a programming language that makes things easier to read or to express, but doesn't add any new functionality. For example, `@my_decorator` is syntactic sugar for `func = my_decorator(func)`. Example: `@timer`
cross-cutting concerns
Aspects of a program that affect many different parts of the system, like logging or security, rather than just one specific feature. Example: `@log_calls`

Recall questions

Understanding checks

What will this code print?

<b>Hello</b>

The decorator `make_bold` wraps `get_text`. When `get_text()` is called, it actually calls `wrapper()`, which evaluates `func()` (returning `'Hello'`), wraps it in `<b>` tags, and returns the result.

This decorator is meant to log the execution of a function, but calling `add(2, 3)` returns `None`. Why?

The `wrapper` function forgot to return the result of `func(*args, **kwargs)`.

In a decorator, the `wrapper` replaces the original function. If the original function calculates a value but the `wrapper` doesn't explicitly return it, the caller will implicitly receive `None`.

Practice tasks

Add Logging to a Decorator

The current `log_call` decorator doesn't do anything yet. Modify it so that it prints `'Calling function'` before executing the decorated function, and `'Function finished'` after. Apply it to the `say_hello` function.

Challenge

Write a timing decorator that accepts arguments

Create a decorator `timer` that measures the execution time of a function. It must accept any number of positional and keyword arguments (`*args`, `**kwargs`), run the original function, print the time taken in seconds (you can mock the time or use `import time`), and return the original function's result.

Continue learning

Previous: Defining functions and return values

Previous: First-class functions: passing and returning functions

Previous: Closures

Next: functools.wraps and preserving metadata

Next: Decorator Factories with Arguments

Return to Python Roadmap