Sunday, August 23, 2026
HomeData SciencePython Decorators for Data Scientists – Practical Guide 2026

Python Decorators for Data Scientists – Practical Guide 2026

Table of Content

Decorators are one of Python’s most powerful features, but many data scientists avoid them because they seem complex. In reality, decorators follow a simple pattern, and once you understand them, you’ll find dozens of practical uses: caching expensive computations, adding logging to functions, validating inputs, timing model training, and retry logic for API calls. This guide demystifies decorators with real data science examples.

How Decorators Work – The Core Pattern

A decorator is a function that takes another function as input, wraps it with extra behaviour, and returns a new function. Python’s @ syntax is just syntactic sugar for this pattern:

# These two are exactly equivalent:

@my_decorator
def my_function():
    pass

# Is the same as:
def my_function():
    pass
my_function = my_decorator(my_function)

Writing Your First Decorator

import functools

def timer(func):
    @functools.wraps(func)  # preserves the original function's name and docstring
    def wrapper(*args, **kwargs):
        import time
        start  = time.perf_counter()
        result = func(*args, **kwargs)
        end    = time.perf_counter()
        print(f"[TIMER] {func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def train_model(X, y):
    from sklearn.ensemble import RandomForestClassifier
    model = RandomForestClassifier(n_estimators=200)
    model.fit(X, y)
    return model

model = train_model(X_train, y_train)
# Output: [TIMER] train_model took 3.2145s

Caching with functools.lru_cache and cache

The most commonly useful decorator for data scientists is caching. When a function is expensive and you call it repeatedly with the same inputs:

from functools import lru_cache, cache

@cache  # Python 3.9+ — unlimited cache
def get_embedding(text: str) -> list:
    '''Call expensive embedding API.'''
    response = embedding_api.encode(text)
    return response.tolist()

# First call hits the API, subsequent calls return cached result instantly
emb1 = get_embedding("machine learning tutorial")  # API call
emb2 = get_embedding("machine learning tutorial")  # cached!

@lru_cache(maxsize=1000)  # Limit cache to 1000 entries
def load_feature_store(date: str) -> dict:
    return read_from_s3(f"features/{date}.parquet")

Retry Decorator for API Calls

import time, functools

def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts - 1:
                        raise
                    wait = delay * (2 ** attempt)  # exponential backoff
                    print(f"Attempt {attempt+1} failed: {e}. Retrying in {wait:.1f}s...")
                    time.sleep(wait)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=1.0, exceptions=(requests.RequestException,))
def call_llm_api(prompt: str) -> str:
    response = requests.post("https://api.example.com/generate", json={"prompt": prompt})
    response.raise_for_status()
    return response.json()["text"]

Validation Decorator

def validate_dataframe(required_cols):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(df, *args, **kwargs):
            import pandas as pd
            if not isinstance(df, pd.DataFrame):
                raise TypeError(f"Expected DataFrame, got {type(df)}")
            missing = set(required_cols) - set(df.columns)
            if missing:
                raise ValueError(f"Missing required columns: {missing}")
            return func(df, *args, **kwargs)
        return wrapper
    return decorator

@validate_dataframe(required_cols=['age', 'income', 'credit_score'])
def preprocess_features(df):
    df['age_bin'] = pd.cut(df['age'], bins=[18, 25, 35, 50, 65, 100])
    return df

Logging Decorator

import logging, functools

logging.basicConfig(level=logging.INFO)

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling {func.__name__} with args={args[:2]}, kwargs={list(kwargs.keys())}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} completed")
        return result
    return wrapper

@log_calls
@timer
def feature_engineering(df, target_col):
    # Both decorators applied: first logs, then times
    ...

Class-Based Decorators

class RateLimiter:
    def __init__(self, calls_per_second=1):
        self.min_interval = 1.0 / calls_per_second
        self.last_called  = 0

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - self.last_called
            if elapsed < self.min_interval:
                time.sleep(self.min_interval - elapsed)
            self.last_called = time.time()
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(calls_per_second=5)
def fetch_stock_price(ticker: str):
    return yfinance.download(ticker, period="1d")

Conclusion

Decorators are not magic — they're just functions that wrap other functions. Once you see the pattern, you'll find yourself reaching for them constantly: caching expensive computations, adding retry logic to unreliable API calls, validating DataFrame schemas, and timing slow pipeline steps. The four lines of boilerplate (def decorator(func), @functools.wraps(func), def wrapper(*args, **kwargs), return wrapper) are worth memorising — they unlock a cleaner, more maintainable codebase.

Leave feedback about this

  • Rating

Durgesh Kekare
Durgesh Kekarehttps://www.dataexpertise.in
Durgesh Kekare is a data science educator and founder of DataExpertise.in. With expertise in Python, machine learning, and analytics, he helps 10,000+ learners break into data careers.

Latest Posts

List of Categories