# Decorators: Adding Behavior to a Function Without Touching It<no value>

## What will we learn?

- What a **Decorator** is and why it is useful.
- How to build a Decorator: a function that receives a function and returns a **wrapper**.
- The **`@`** syntax.
- A wrapper that fits **any** function, with `*args` and `**kwargs`.

## Why do we need this?

We want to print a message every time a function is called — for debugging:

```python
def add(a, b):
    print("Calling add")
    return a + b

def multiply(a, b):
    print("Calling multiply")
    return a * b
```

The problems:

- **The same line is copied** into every function — again the duplication DRY forbids.
- **The logic gets mixed** with helper code that has nothing to do with it.
- To **remove** the prints, you have to go over all the functions and delete them.

A **Decorator** lets you write the extra behavior **once**, and put it on any function with one line.

## Explanation from the basics

### What a Decorator is built from

A Decorator combines two things we learned in the previous lessons:

- **Higher-Order Function** — it receives a function and returns a function.
- **Closure** — the function it returns remembers the original function.

The structure is always the same:

1. The Decorator receives the original function (`func`).
2. Inside it you define a new function — the **`wrapper`** — that does something extra, **and calls `func`**.
3. The Decorator returns `wrapper` — **without brackets**.

### The @ syntax

```python
@log_call
def add(a, b):
    return a + b
```

This is exactly the same as writing after the definition:

```python
add = log_call(add)
```

That is: the name `add` now points to the **wrapper**, and the wrapper calls the original `add`.

{{< box "analogy" "Analogy: wrapping a gift" >}}
The gift (the function) does not change. You wrap it in paper (the wrapper) — and whoever receives it meets the wrapping first, and only then the gift. You can wrap any gift in the same kind of paper, and you can take the wrapping off without hurting the gift.
{{< /box >}}

## Syntax

```python
def my_decorator(func):              # 1. receive the original function
    def wrapper(*args, **kwargs):    # 2. a new function that wraps it
        # ... something before ...
        result = func(*args, **kwargs)
        # ... something after ...
        return result                # return the original result
    return wrapper                   # 3. return the wrapper (no parentheses)

@my_decorator                        # same as: my_function = my_decorator(my_function)
def my_function():
    ...
```

## First code example

`week-2/log_call.py`:

```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def multiply(a, b):
    return a * b

print(add(2, 3))
print(multiply(4, 5))
```

**Output:**

```text
Calling add
5
Calling multiply
20
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `def log_call(func):` | The Decorator receives the original function. |
| `def wrapper(a, b):` | The wrapping — it receives the same parameters as the original. |
| `print("Calling", func.__name__)` | The extra behavior. `__name__` is the name of the function. |
| `return func(a, b)` | Call the original, and **return** its result. |
| `return wrapper` | The Decorator returns the wrapper — without brackets. |
| `@log_call` | `add = log_call(add)`. From now on `add(2, 3)` runs the wrapper. |

## Another example

**A wrapper that fits any function.** The previous wrapper works only with functions that receive exactly `a, b`. With `*args` and `**kwargs` it receives **any** parameter and passes it on. `week-2/log_any.py`:

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__, "with", args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**Output:**

```text
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60
```

**A useful example: measuring time.** `week-2/timed.py`:

```python
import time

def timed(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        seconds = round(time.time() - start, 2)
        print(func.__name__, "took", seconds, "seconds")
        return result
    return wrapper

@timed
def slow_sum(n):
    time.sleep(0.5)
    return sum(range(n))

print(slow_sum(1000))
```

**Output:**

```text
slow_sum took 0.5 seconds
499500
```

(The exact time varies a little between runs.) The same `@timed` can be put on any function in the program — without changing a single line inside it.

## Common mistake

**Two mistakes that happen to almost everyone who writes a first Decorator:**

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        func(*args, **kwargs)
    return wrapper()
```

1. **No `return` inside the wrapper.** The original function runs, but its result is lost — and the decorated function always returns `None`. The fix: `return func(*args, **kwargs)`.
2. **`return wrapper()` with brackets.** This **calls** the wrapper already at definition time, and returns its result instead of the function. The fix: `return wrapper`.

**The rule:** the wrapper returns **the result**, and the Decorator returns **the wrapper**.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

### Exercise 1: A first Decorator

Write a Decorator named `log_call` that prints `Calling <name>` before every call to the function, and apply it with `@` to `add` and to `multiply`.

```python
print(add(2, 3))
print(multiply(4, 5))
```

**Expected output:**

```text
Calling add
5
Calling multiply
20
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper


@log_call
def add(a, b):
    return a + b


@log_call
def multiply(a, b):
    return a * b


print(add(2, 3))
print(multiply(4, 5))
```

```text
Calling add
5
Calling multiply
20
```

`@log_call` above `def add` is equivalent to `add = log_call(add)`. From now on `add` points to the wrapper.
{{< /solution >}}

### Exercise 2: A wrapper that fits any function

The previous wrapper works only with functions that receive exactly two parameters. Rewrite `log_call` so that it works with any function.

```python
@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**Expected output:**

```text
Calling greet
Hi, Dana
Calling total
60
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper


@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name


@log_call
def total(*prices):
    return sum(prices)


print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

```text
Calling greet
Hi, Dana
Calling total
60
```

`*args` and `**kwargs` collect all the arguments, and `func(*args, **kwargs)` passes them on as they are.
{{< /solution >}}

### Exercise 3: Checking the input

Write a Decorator named `positive_only` for functions that receive one number: if the number is negative, it throws a `ValueError` and does not call the function at all. The message includes the function's name (`func.__name__`) and the number that was received, as in the expected output.

```python
@positive_only
def square_root(n):
    return n ** 0.5

print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

**Expected output:**

```text
4.0
error: square_root needs a positive number, got -4
```

{{< solution >}}
```python
def positive_only(func):
    def wrapper(n):
        if n < 0:
            raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
        return func(n)
    return wrapper


@positive_only
def square_root(n):
    return n ** 0.5


print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

```text
4.0
error: square_root needs a positive number, got -4
```

The check happens **before** the call to `func` — if the input is invalid, the original function does not run at all.
{{< /solution >}}

### Exercise 4: Counting calls

Write a Decorator named `count_calls`, which counts how many times the function was called. On every call, **before** the function runs, it prints the function's name and the number of the call — as in the expected output.

```python
@count_calls
def say_hi():
    print("Hi")

say_hi()
say_hi()
```

**Expected output:**

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

{{< solution >}}
```python
def count_calls(func):
    count = 0

    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(func.__name__, "call #" + str(count))
        return func(*args, **kwargs)

    return wrapper


@count_calls
def say_hi():
    print("Hi")


say_hi()
say_hi()
```

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

`count` belongs to `count_calls`, and the wrapper remembers it (a Closure). Because it **changes** `count`, it needs `nonlocal`.
{{< /solution >}}

### Exercise 5: Two mistakes

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

The Decorator is supposed to return the function's result in capital letters. Find two mistakes and fix them.

```python
@shout
def greet(name):
    return "hello " + name

print(greet("dana"))
```

**Expected output:**

```text
HELLO DANA
```

{{< solution >}}
1. **`return wrapper()` with brackets** — calls the wrapper already at definition time, instead of returning it. It should be `return wrapper`.
2. **The wrapper returns nothing** — `result.upper()` creates a new string, but it is not returned. It should be `return result.upper()`.

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


@shout
def greet(name):
    return "hello " + name


print(greet("dana"))
```

```text
HELLO DANA
```

The rule: the wrapper returns **the result**, and the Decorator returns **the wrapper**.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Decorator** = a function that receives a function, wraps it in a **wrapper** with extra behavior, and returns the wrapper.
- It is built from a **Higher-Order Function** and a **Closure** — the two topics of the previous lessons.
- `@decorator` above a `def` = `func = decorator(func)`.
- With `*args` and `**kwargs`, the wrapper fits any function.
- The wrapper returns **the result** (`return func(...)`), and the Decorator returns **the wrapper** (`return wrapper`, without brackets).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Glossary — decorator](https://docs.python.org/3/glossary.html#term-decorator)
- [Function definitions](https://docs.python.org/3/reference/compound_stmts.html#function-definitions)
- [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps)

<!-- docs:end -->
