OSPython.018: Decorators Basics

Python code editor showing a decorator above a function on a dark screen

A decorator wraps a function so you can add behavior without rewriting the function itself.

That sounds advanced, but the basic idea is simple: take a function, put another function around it, and return the wrapped version.

Start with the smallest useful example

def announce(func):
    def wrapper():
        print("Starting...")
        func()
        print("Finished.")
    return wrapper

@announce
def greet():
    print("Hello!")

greet()

Output:

Starting...
Hello!
Finished.

@announce tells Python to pass greet into the announce() decorator and replace greet with the returned wrapper.

Video 1: Python decorators from the ground up

Corey Schafer builds decorators step by step and shows why the wrapper pattern works.

What the @ syntax really means

This:

@announce
def greet():
    print("Hello!")

is essentially a cleaner way to write:

def greet():
    print("Hello!")

greet = announce(greet)

The @ form is easier to read once you recognize what Python is doing.

Decorating functions that take arguments

A wrapper often needs to accept whatever arguments the original function receives. *args and **kwargs make that possible.

def announce(func):
    def wrapper(*args, **kwargs):
        print("Starting...")
        result = func(*args, **kwargs)
        print("Finished.")
        return result
    return wrapper

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

print(add(2, 3))

Output:

Starting...
Finished.
5

Video 2: A focused decorators lesson

Tech With Tim explains how decorators modify function behavior without changing the original function body.

Preserve the original function’s information

A wrapper is technically a new function. Without help, metadata such as the original function name can be lost. Python’s functools.wraps is the standard fix.

from functools import wraps

def announce(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Starting...")
        return func(*args, **kwargs)
    return wrapper

For real projects, using @wraps(func) inside your decorator is a good habit.

Where decorators are useful

  • Logging: record when a function runs.
  • Timing: measure how long work takes.
  • Authentication: check permission before a protected action.
  • Caching: reuse a previous result instead of recalculating it.
  • Validation: check inputs before the main function runs.

A simple timer decorator

from functools import wraps
from time import perf_counter

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        result = func(*args, **kwargs)
        elapsed = perf_counter() - start
        print(f"{func.__name__}: {elapsed:.6f} seconds")
        return result
    return wrapper

@timer
def work():
    return sum(range(100000))

work()

The useful part is separation: work() contains the work, while @timer handles timing.

Video 3: Common decorators you will see in Python

Tech With Tim demonstrates practical decorators including property, staticmethod, classmethod, caching, and dataclass-related patterns.

How this connects to the previous lesson

OSPython.017: Class Variables and Instance Variables separated data that belongs to the class from data that belongs to each object. Decorators are another Python tool for organizing behavior cleanly instead of repeating the same logic everywhere.

Common beginner mistakes

  • Calling the decorated function while defining the decorator instead of passing the function itself.
  • Forgetting to return the wrapper.
  • Forgetting to return the original function’s result from the wrapper.
  • Writing a wrapper with no *args or **kwargs when the original function needs arguments.
  • Skipping functools.wraps in reusable decorators.

Quick practice

  1. Create a function named hello(name).
  2. Create a decorator named log_call.
  3. Make the wrapper print Calling function... before the original function runs.
  4. Use *args and **kwargs.
  5. Add @log_call above hello.
  6. Call hello("Ada") and verify both messages appear.

Key takeaway

A decorator takes a callable, adds or changes behavior around it, and returns a callable. The @decorator syntax makes that wrapping relationship easy to see.

Leave a comment