When to use decorators with timer() — timing a function via a decorator is the most elegant and reusable way to measure execution time in Python, especially when you want consistent profiling across multiple functions without cluttering their code with manual time.perf_counter() calls. Use a @timer decorator when: you have several functions to benchmark (e.g., data processing steps, API endpoints, ML model inference), you want automatic logging of timings in production or notebooks, you need exception-safe timing (cleanup always runs), or you want to separate performance measurement from business logic. In 2026, decorator-based timing is standard in data pipelines, web services, ML training loops, and optimization workflows — it’s cleaner than manual timing, more maintainable than inline timeit, and integrates beautifully with logging, pandas/Polars profiling, and observability tools.
Here’s a complete, practical guide to when and how to use @timer decorators in Python: ideal use cases, implementation patterns, comparison to manual/timeit/context managers, real-world examples, and modern best practices with type hints, logging, multiple runs, and pandas/Polars integration.
When to use a @timer decorator vs alternatives:
- Use decorator when: timing many functions repeatedly, want zero boilerplate in timed code, need exception-safe cleanup, or want integrated logging (e.g., production monitoring).
- Use manual
perf_counter()when: one-off measurement, very simple script, or need maximum control over timing scope. - Use
timeitwhen: micro-benchmarking small snippets, comparing implementations precisely, or measuring many runs automatically (disables GC). - Use context manager when: timing code blocks (not just functions), setup/teardown needed, or non-function code (e.g., pandas operations).
Simple timer decorator — measures wall-clock time with perf_counter(), preserves metadata with @wraps.
from functools import wraps
import time
def timer(func):
"""Decorator that prints execution time of the function."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.6f} seconds")
return result
return wrapper
Real-world pattern: timing pandas/Polars transformations — decorate heavy operations for profiling without changing logic.
import pandas as pd
@timer
def group_and_sum(df: pd.DataFrame) -> pd.DataFrame:
"""Group by category and sum values."""
return df.groupby('category').sum()
# Usage
df = pd.DataFrame({'category': ['A','A','B'], 'value': [10,20,30]})
result = group_and_sum(df)
# group_and_sum took 0.001234 seconds
Best practices make timer decorators safe, readable, and useful. Always use @wraps(func) — preserves function name, docstring, type hints. Prefer time.perf_counter() — high-resolution wall-clock time. Modern tip: use Polars lazy API — decorate .collect() or .sink_* calls to profile expensive execution. Add type hints — def timer(func: Callable) -> Callable — improves clarity and mypy checks. Use logging instead of print — logging.info(f"{func.__name__} took {elapsed:.6f}s") — better for production. Time multiple runs — average over 10–100 executions for accuracy. Use contextmanager alternative — for timing blocks. Combine with codetiming or pyinstrument — advanced profiling. Disable GC if needed — but timeit does it automatically for micro-benchmarks. Test timing — assert elapsed time roughly correct in integration tests. Stack with other decorators — order matters (@timer @log vs @log @timer). Use time.process_time() — for CPU-only timing if I/O/sleep should be excluded.
Use a @timer decorator when you want automatic, reusable, exception-safe timing across many functions — it’s cleaner than manual code and more flexible than inline timeit. In 2026, combine with logging, type hints, Polars profiling, and multiple runs for reliable performance insights. Master timer decorators, and you’ll profile, optimize, and monitor Python code effortlessly and accurately.
Next time you need to know how long a function takes — decorate it with @timer. It’s Python’s cleanest way to say: “Measure this automatically — no extra code inside.”