# 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:

```python
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
```

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."

```python
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
```

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.

```python
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
```

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.

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

@add_logging
def search_catalog(query):
    return f"Searching for: {query}"
```

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:

```python
print(catalog_book.__name__)  # "wrapper" - not helpful!
```

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

```python
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
```

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:

```python
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}"
```

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:**

```python
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
```

**Caching results:**

```python
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))
```

**Validating inputs:**

```python
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"
```

## The Class Method Decorators

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

```python
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)
```

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*](https://www.tech-reader.blog) *and the author of* [*Think Like a Genius*](https://amazon.com/author/aaron.rose)*.*
