DEV Community

Cover image for The Function Modification Station: Basic Decorator Mechanics
Aaron Rose
Aaron Rose

Posted on

The Function Modification Station: Basic Decorator Mechanics

Timothy had mastered functions that remembered their environment through closures, but Margaret had one more revelation waiting. She led him to a workshop labeled "The Function Modification Station"—a place where existing procedures could be enhanced with new capabilities without rewriting them.

The Enhancement Request

The head librarian approached Timothy with a problem: "Every cataloging function needs to log when it runs and how long it takes. Can you add this to all our procedures without changing their core logic?"

Timothy's first instinct was to modify each function directly:

import time

def catalog_book(title, author):
    start_time = time.time()
    print(f"Running catalog_book at {start_time}")

    # Actual cataloging work
    result = f"Cataloged: {title} by {author}"

    end_time = time.time()
    print(f"Completed in {end_time - start_time:.2f} seconds")
    return result
Enter fullscreen mode Exit fullscreen mode

But with dozens of functions needing this same enhancement, copying the logging code everywhere felt wrong. Margaret showed him a better way.

The Wrapper Concept

"What if," Margaret suggested, "we could wrap one function inside another? The outer function adds the logging, while the inner function does the original work."

def add_logging(original_function):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print(f"Running {original_function.__name__}")

        result = original_function(*args, **kwargs)

        elapsed = time.time() - start_time
        print(f"Completed in {elapsed:.2f} seconds")
        return result

    return wrapper
Enter fullscreen mode Exit fullscreen mode

Timothy studied the pattern. The add_logging function accepted another function as input and returned a new function that wrapped the original with logging code.

def catalog_book(title, author):
    return f"Cataloged: {title} by {author}"

# Wrap the function
catalog_book = add_logging(catalog_book)

# Now it logs automatically
catalog_book("1984", "Orwell")
# Running catalog_book
# Completed in 0.00 seconds
Enter fullscreen mode Exit fullscreen mode

The enhanced version executed the logging code before and after the original function, but the cataloging logic remained unchanged.

The @ Syntax Sugar

Margaret revealed Python's shorthand for this pattern: the decorator syntax.

@add_logging
def catalog_book(title, author):
    return f"Cataloged: {title} by {author}"

@add_logging
def search_catalog(query):
    return f"Searching for: {query}"
Enter fullscreen mode Exit fullscreen mode

The @add_logging line was equivalent to catalog_book = add_logging(catalog_book), but cleaner and more explicit. The decorator sat right above the function definition, announcing "this function is enhanced with logging."

The Preservation Challenge

Timothy discovered a problem when inspecting decorated functions:

print(catalog_book.__name__)  # "wrapper" - not helpful!
Enter fullscreen mode Exit fullscreen mode

The wrapper function had replaced the original's metadata. Margaret showed him the solution:

from functools import wraps

def add_logging(original_function):
    @wraps(original_function)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print(f"Running {original_function.__name__}")

        result = original_function(*args, **kwargs)

        elapsed = time.time() - start_time
        print(f"Completed in {elapsed:.2f} seconds")
        return result

    return wrapper
Enter fullscreen mode Exit fullscreen mode

The @wraps decorator preserved the original function's name, docstring, and other metadata, making debugging and introspection work correctly.

The Multiple Enhancement Pattern

Timothy learned decorators could be stacked:

def add_logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def require_permission(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not user_has_permission():
            raise PermissionError("Access denied")
        return func(*args, **kwargs)
    return wrapper

@add_logging
@require_permission
def delete_book(book_id):
    return f"Deleted book {book_id}"
Enter fullscreen mode Exit fullscreen mode

The decorators applied from bottom to top: require_permission wrapped delete_book first, then add_logging wrapped the result. The function gained both permission checking and logging.

The Practical Applications

Timothy compiled common decorator patterns:

Timing functions:

def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__}: {time.time() - start:.4f}s")
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

Caching results:

def cache_results(func):
    cached = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cached:
            cached[args] = func(*args)
        return cached[args]
    return wrapper

@cache_results
def expensive_calculation(n):
    return sum(range(n))
Enter fullscreen mode Exit fullscreen mode

Validating inputs:

def validate_positive(func):
    @wraps(func)
    def wrapper(number):
        if number <= 0:
            raise ValueError("Number must be positive")
        return func(number)
    return wrapper

@validate_positive
def process_count(count):
    return f"Processing {count} items"
Enter fullscreen mode Exit fullscreen mode

The Class Method Decorators

Margaret showed Timothy that Python included built-in decorators for class methods:

class Library:
    total_books = 0

    @classmethod
    def get_total_books(cls):
        return cls.total_books

    @staticmethod
    def format_title(title):
        return title.upper()

    @property
    def book_count(self):
        return len(self.books)
Enter fullscreen mode Exit fullscreen mode

These decorators transformed how methods behaved—@classmethod received the class itself, @staticmethod acted like a regular function, and @property made methods accessible like attributes.

The Mental Model

Timothy developed a clear understanding: decorators were functions that transformed other functions. They followed a consistent pattern:

  1. Accept a function as input
  2. Create a wrapper function that adds behavior
  3. Return the wrapper to replace the original
  4. Use @wraps to preserve metadata

The @ syntax was just syntactic sugar for the function wrapping pattern, making the enhancement visible and declarative.

Timothy's Decorator Wisdom

Through mastering the Function Modification Station, Timothy learned essential principles:

Decorators enhance without changing core logic: Add cross-cutting concerns (logging, timing, validation) without modifying function internals.

The @ syntax is transformation: @decorator means "replace this function with the decorated version."

Preserve metadata with @wraps: Always use functools.wraps to maintain function identity.

Decorators stack bottom-up: Multiple decorators apply from bottom to top.

Common patterns emerge: Timing, caching, validation, and access control are natural decorator use cases.

Timothy's exploration revealed that decorators embodied Python's philosophy: make common patterns easy to express. Rather than copying enhancement code across functions, decorators let you declare enhancements once and apply them declaratively. The Function Modification Station transformed ordinary procedures into enhanced versions while keeping the core logic clean and focused.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)