Access to the original function is the key power of decorators in Python — when you write a decorator, the original function is passed as an argument to the decorator function, giving you full access to it inside the wrapper. This lets you call the original function (with its original arguments and return value), add behavior before/after, modify inputs/outputs, handle exceptions, log details, or even skip execution — all without changing the decorated function’s code. In 2026, this access enables decorators to become the standard for cross-cutting concerns: logging, timing, caching, authentication, validation, retry logic, rate limiting, instrumentation, and more — keeping business logic clean while adding observability, safety, and performance features transparently.
Here’s a complete, practical guide to accessing and using the original function inside a decorator: basic wrapper pattern, preserving metadata with @wraps, modifying arguments/results, handling exceptions, real-world patterns, and modern best practices with type hints, functools, logging, and pandas/Polars integration.
Basic access — decorator receives the original function as func, wrapper calls it with func(*args, **kwargs).
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before calling the original function")
result = func(*args, **kwargs) # access & call original
print("After calling the original function")
return result
return wrapper
@my_decorator
def say_hello(name: str) -> str:
return f"Hello, {name}!"
print(say_hello("Alice"))
# Before calling the original function
# After calling the original function
# Hello, Alice!
Preserving metadata — use @functools.wraps(func) — otherwise wrapper loses original name, docstring, type hints, annotations.
from functools import wraps
def my_decorator(func):
@wraps(func) # copies __name__, __doc__, __annotations__, etc.
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # greet (not wrapper)
print(greet.__doc__) # Greet someone.
print(greet("Bob")) # Hello, Bob!
Real-world pattern: timer decorator for pandas/Polars functions — access original to time execution and return its result unchanged.
import time
from functools import wraps
import pandas as pd
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # call original, keep result
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.6f} seconds")
return result
return wrapper
@timer
def group_and_sum(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby('category').sum()
df = pd.DataFrame({'category': ['A','A','B'], 'value': [10,20,30]})
result = group_and_sum(df)
# group_and_sum took 0.001234 seconds
print(result)
Best practices make decorator access safe, readable, and powerful. Always use @wraps(func) — preserves metadata for debugging/introspection. Use *args, **kwargs — makes decorator generic (works with any signature). Modern tip: use Polars for high-performance transformations — decorate Polars lazy frames or expressions for timing/logging. Add type hints — def decorator(func: Callable) -> Callable — improves clarity and mypy checks. Return the original result — chainable decorators. Handle exceptions — wrap in try/except if needed (e.g., retry). Use contextlib.ContextDecorator — class-based decorators that work as context managers. Test decorators independently — wrap dummy functions and assert behavior. Combine with tenacity — retry decorators for transient errors. Use logging inside decorators — centralize logging without cluttering functions. Avoid decorators on generators — can break iteration; use wrappers carefully. Stack decorators thoughtfully — order matters (@timer @log vs @log @timer).
Decorators have full access to the original function — call it, modify inputs/outputs, handle exceptions, add logging/timing — all transparently. In 2026, use @wraps, type hints, generic args/kwargs, Polars profiling, and test thoroughly. Master decorator access, and you’ll write clean, reusable, observable Python code that enhances functions elegantly and efficiently.
Next time you need to extend a function’s behavior — write a decorator. It’s Python’s cleanest way to say: “I’ll wrap this function and add my magic — without touching its code.”