# Decorators — Part 1: The Core Mechanism

## 1. A Decorator Is Just a Function That Wraps Another Function

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def greet(name):
    return f"hello, {name}"

greet = shout(greet)   # manually "decorating" — reassign the name
print(greet("ana"))     # "HELLO, ANA"
```

`shout` takes a function and returns a new function (`wrapper`) that calls the original and modifies its result. This pattern — wrap, call the original, do something before/after/with the result — is the entire idea behind decorators. The `@` syntax is sugar for exactly the reassignment shown above.

## 2. The `@` Syntax

```python
def shout(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("ana"))  # "HELLO, ANA"
```

```python
@shout
def greet(name):
    return f"hello, {name}"

# is EXACTLY equivalent to:

def greet(name):
    return f"hello, {name}"
greet = shout(greet)
```

`@decorator_name` above a `def` runs `decorator_name(the_function_just_defined)` immediately after the function is compiled, and rebinds the name to whatever that call returns. This happens **once, at definition time** — not on every call to `greet`.

## 3. Preserving Metadata with `functools.wraps`

```python
import functools

def shout(func):
    @functools.wraps(func)   # copies __name__, __doc__, etc. from func onto wrapper
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Return a greeting."""
    return f"hello, {name}"

print(greet.__name__)  # "greet" — WITHOUT @wraps this would be "wrapper"
print(greet.__doc__)    # "Return a greeting."
```

Without `functools.wraps`, the decorated function's `__name__`, `__doc__`, and other introspection metadata get silently replaced by `wrapper`'s — breaking debuggers, help(), and any code that inspects the function. Always use `@functools.wraps(func)` inside a decorator's inner function.

## 4. Multiple Decorators Stack Bottom-Up

```python
def bold(func):
    def wrapper(*a, **kw):
        return f"<b>{func(*a, **kw)}</b>"
    return wrapper

def italic(func):
    def wrapper(*a, **kw):
        return f"<i>{func(*a, **kw)}</i>"
    return wrapper

@bold
@italic
def text():
    return "hi"

print(text())  # "<b><i>hi</i></b>"
```

Stacked decorators apply **bottom-to-top** at definition time (the one closest to the function runs first, wrapping it; the next one wraps that result), but that means the resulting **call order is outside-in**: `bold`'s wrapper runs first when you call `text()`, and it calls into `italic`'s wrapper, which calls the real `text`.

## 5. Common Uses

```python
import time, functools

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timed
def slow_add(a, b):
    time.sleep(0.1)
    return a + b
```

Common real-world decorators: logging/timing (above), caching (`functools.lru_cache`), access control/authentication checks, retry logic, input validation, and registering a function into some registry (e.g. route decorators in web frameworks like Flask/FastAPI).

## 6. Common Pitfalls

1. **Forgetting `functools.wraps`** — breaks introspection, `help()`, and anything relying on `__name__`/`__doc__`; always include it.
2. **Forgetting `*args, **kwargs` in the wrapper** — a wrapper defined as `def wrapper(): ...` can only decorate zero-argument functions; almost always write `def wrapper(*args, **kwargs)` unless you specifically need a fixed signature.
3. **Assuming decorators run on every call** — the decorator function itself runs exactly once, at definition time; only the returned `wrapper` runs on every call.
4. **Misreading stacking order** — `@bold` then `@italic` above a function applies `italic` first (closest to the def), but `bold`'s wrapper executes first at call time (outermost).

Continue to `py_decorators_theory_2.md` for decorators with arguments, class decorators, and the bytecode-level mechanics.
