# 10 Exercises to Master Python Decorators (With Solutions)

You've read the theory. You understood how decorators work. Now it's time to prove to yourself.

These exercises are ordered deliberately. The first few build the foundation skills, and each one adds a layer until you're writing production-style decorators by the end. Try each exercise before looking at the solution. Struggling is where the learning happens.

Let's go.

* * *

## Exercise 1: Functions as Objects

**Task**: Write a function called `apply` that takes a function and a value, and returns the result of calling that function with that value.

```python
def square(x):
    return x * x

def double(x):
    return x * 2

# Your code should make this work:
print(apply(square, 5))   # 25
print(apply(double, 5))   # 10
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>What this tests</strong>: Can you pass a function as an argument and call it?</div>
</div>

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">def apply(func, value):
    return func(value)</code></pre><p>Simple. <code>func</code> is just a variable that happens to hold a function. Call it with <code>()</code> and pass the value.</p>
</details>

* * *

## Exercise 2: Returning a Function from a Function

**Task**: Write a function called `multiplier` that takes a number `n` and returns a *new* function that multiplies any number by `n`.

```python
# Your code should make this work:
times_three = multiplier(3)
times_ten = multiplier(10)
print(times_three(5))   # 15
print(times_ten(5))     # 50
print(times_three(7))   # 21
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>What this tests</strong>: Can you create a closure, a function that remembers a value from its enclosing scope?</div>
</div>

**Common mistake:** Writing two separate functions `times_three` and `times_ten` by hand. The point is that `multiplier` creates them dynamically.

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python"> def multiplier(n):
    def inner(x):
        return x * n
    return inner</code></pre><p><code>inner</code> remembers <code>n</code> from when it was created. This is a closure, the exact mechanism that powers decorators.</p>
</details>

* * *

## Exercise 3: Your First Decorator

**Task**: Write a decorator called `shout` that converts the return value of a function to uppercase.

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

@shout
def farewell(name):
    return f"goodbye, {name}"

print(greet("Moussa"))     # HELLO, MOUSSA
print(farewell("Moussa"))  # GOODBYE, MOUSSA
```

**What this tests**: Can you write a basic decorator that transforms a return value?

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">from functools import wraps
</code></pre><p><code>def shout(func):<br>wraps(func)<br>def wrapper(*args, **kwargs):<br>result = func(*args, **kwargs)<br>return result.upper()<br>return wrapper</code></p><p>Notice the pattern: call the function, catch the result, transform it, return the transformed version.</p><p></p>
</details>

* * *

## Exercise 4: A Counting Decorator

**Task**: Write a decorator called `count_calls` that tracks how many times a function has been called. After each call, print the count.

```python
@count_calls
def say_hello():
    print("Hello!")

say_hello()
# Hello!
# say_hello has been called 1 time(s)

say_hello()
# Hello!
# say_hello has been called 2 time(s)

say_hello()
# Hello!
# say_hello has been called 3 time(s)
```

**What this tests**: Can you maintain state across function calls using function attributes?

**Hint**: Functions are objects, so you can attach attributes to them. Try `wrapper.calls = 0` to initialize a counter on the wrapper function itself. This counter will persist between calls because it lives on the function object, not inside the function body.

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">from functools import wraps
</code></pre><p><code>def count_calls(func):<br>wraps(func)<br>def wrapper(*args, **kwargs):<br>wrapper.calls += 1<br>result = func(*args, **kwargs)<br>print(f"{func.name} has been called {wrapper.calls} time(s)")<br>return result<br>wrapper.calls = 0<br>return wrapper</code></p><p><code>wrapper.calls</code> is set to <code>0</code> once when the decorator is applied. Each time <code>wrapper()</code> runs, it increments the counter. The counter lives on the function object, not as a local variable, so it persists between calls.</p><p></p>
</details>

* * *

## Exercise 5: A Before-and-After decorator

**Task**: Write a decorator called `surround` that adds a line of dashes before and after the function's output.

```python
@surround
def introduce(name, job):
    return f"Hi, I'm {name} and I work as a {job}."

print(introduce("Moussa", "Software Engineer"))
# --------------------
# Hi, I'm Moussa and I work as a Software Engineer.
# --------------------
```

**What this tests**: Can you add behavior before and after a function call?

**Design question**: Should you use `print()` for the dashes or build them into the return value? Think about which approach would compose better with other decorators.

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python"># Side-effect version (simpler but doesn't compose well):
def surround(func):
    def wrapper(*args, **kwargs):
        print("-" * 20)
        result = func(*args, **kwargs)
        print("-" * 20)
        return result
    return wrapper
</code></pre><h1><code>Return-value version (composes well with other decorators):</code></h1><p><code>def surround(func):<br>def wrapper(*args, **kwargs):<br>result = func(*args, **kwargs)<br>return f"{'-' * 20}\n{result}\n{'-' * 20}"<br>return wrapper</code></p><p>The return-value version is better design. If you stack this with another decorator that transforms return values, everything works smoothly. The side-effect version's dashes can't be intercepted by other decorators.</p><p></p>
</details>

* * *

## Exercise 6: Input Validation

**Task**: Write a decorator called `positive_only` that checks if all arguments passed to a function are positive numbers. If any argument is negative or zero, print an error message and don't call the function.

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

print(add(3, 5))    # 8
print(add(-1, 5))   # Error: all arguments must be positive!
                     # None
print(add(2, -4))   # Error: all arguments must be positive!
                     # None
```

**What this tests**: Can you add conditional logic, running the function only when certain conditions are met?

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">from functools import wraps
</code></pre><p><code>def positive_only(func):<br>@wraps(func)<br>def wrapper(*args, **kwargs):<br>for arg in args:<br>if arg &lt;= 0:<br>print("Error: all arguments must be positive!")<br>return None<br>return func(*args, **kwargs)<br>return wrapper</code></p><p>The loop checks each argument. If any fails, we return early without ever calling the original function. Only if all checks pass do we forward the call.</p><p></p>
</details>

* * *

## Exercise 7: Decorators with Arguments - `@slow_down(seconds)`

**Task:** Write a decorator called `slow_down` that takes a number of seconds and waits that long before calling the function.

```python
import time

@slow_down(2)
def greet(name):
    print(f"Hello, {name}!")

greet("Moussa")
# (waits 2 seconds)
# Hello, Moussa!
```

**What this tests:** Can you add the extra layer needed for decorator arguments?

**Remember:** `@slow_down(2)` means Python calls `slow_down(2)` first, which must return a decorator. So you need three layers: the outer function takes the argument, the middle function takes the function, and the inner function replaces the function.

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">import time
from functools import wraps
</code></pre><p><code>def slow_down(seconds):<br>def decorator(func):<br>@wraps(func)<br>def wrapper(*args, **kwargs):<br>time.sleep(seconds)<br>return func(*args, **kwargs)<br>return wrapper<br>return decorator</code></p><p>Three layers: <code>slow_down(seconds)</code> → <code>decorator(func)</code> → <code>wrapper(*args, **kwargs)</code>. Each layer remembers the values from the layer above via closures.</p><p></p>
</details>

* * *

## Exercise 8: Decorator with Arguments - `@repeat(n)`

**Task**: Write a `repeat` decorator that takes a number `n` and runs the function `n` times. It should return the result of the last call.

```python
@repeat(4)
def say_hi(name):
    print(f"Hi, {name}!")

say_hi("Moussa")
# Hi, Moussa!
# Hi, Moussa!
# Hi, Moussa!
# Hi, Moussa!
```

**What this tests**: Combining decorator arguments with loop logic.

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Be careful where you place the <code>return</code> statement. A <code>return</code> inside a <code>for</code> loop exits the function immediately on the first iteration, the loop won't continue.</div>
</div>

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator</code></pre><p>Two important details:</p><ol><li><p><code>result = None</code> before the loop handles the edge case where <code>n</code> is 0</p></li><li><p><code>return result</code> is <em>after</em> the loop, not inside it. Putting <code>return</code> inside the loop would exit on the first iteration.</p></li></ol>
</details>

* * *

## Exercise 9: Stacking Decorators

**Task**: Using the `shout` decorator from Exercise 3 and the `surround` decorator from Exercise 5, predict the output of this code **before running it**.

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

result = greet("Moussa")
print(result)
```

Then swap the order:

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

result = greet("Moussa")
print(result)
```

**Question:** Why are the outputs different? Does one of them produce unexpected results?

**What this tests**: Do you understand the execution order of stacked decorators?

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<p><strong>With the side-effect version of </strong><code>surround</code><strong>:</strong></p><p>First version (<code>@surround</code> on top of <code>@shout</code>):</p><pre class="not-prose"><code class="language-plaintext">--------------------
--------------------
HELLO, MOUSSA</code></pre><p>The dashes print as side effects (immediately), while the string travels silently through the return chain. By the time <code>print(result)</code> runs, both decorators are done and only the final string appears.</p><p>Second version (<code>@shout</code> on top of <code>@surround</code>): The same visual issue, dashes appear separately from the content.</p><p></p><p><strong>With the return-value version of </strong><code>surround</code><strong>:</strong></p><p>First version (<code>@surround</code> on top of <code>@shout</code>):</p><pre class="not-prose"><code class="language-plaintext">--------------------
HELLO, MOUSSA
--------------------</code></pre><p>Second version (<code>@shout</code> on top of <code>@surround</code>):</p><pre class="not-prose"><code class="language-plaintext">--------------------
HELLO, MOUSSA
--------------------</code></pre><p>(The dashes get uppercased too, but <code>-</code> has no uppercase, so they look the same.)</p><p><strong>The lesson</strong>: Decorators apply bottom-up. The bottom one wraps first. Order matters, especially when mixing side effect and return values. Prefer the return-value approach for better composition.</p>
</details>

* * *

## Exercise 10: The Boss Challenge - Build a Cache

**Task**: Write a decorator called `cache` that remembers the results of previous function calls. If the function is called again with the same arguments, return the saved result instead of running the function again.

```python
import time

@cache
def slow_add(a, b):
    time.sleep(2)  # pretend this is expensive
    return a + b

print(slow_add(2, 3))  # (waits 2 seconds) → 5
print(slow_add(2, 3))  # (instant!) → 5
print(slow_add(1, 1))  # (waits 2 seconds) → 2
print(slow_add(1, 1))  # (instant!) → 2
```

**What this tests**: Maintaining state (a dictionary of previous results) across calls, and using tuple arguments as dictionary keys.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Use dictionary to store results. The key can be <code>args</code> since tuples are hashable. Store the dictionary as an attribute on the wrapper function, just like the counter in Exercise 4.</div>
</div>

**Important**: The decorator should return the raw value (like `5`), not a formatted string (like `"(cached) -> 5"`). The caller shouldn't know or care that caching is happening.

<details data-node-type="hn-details-summary">
<summary>Click to reveal solution</summary>
<pre class="not-prose"><code class="language-python">def cache(func):
    def wrapper(*args):
        if args in wrapper.memory:
            return wrapper.memory[args]
        result = func(*args)
        wrapper.memory[args] = result
        return result
    wrapper.memory = {}
    return wrapper</code></pre><p><code>wrapper.memory</code> is a dictionary that maps argument tuples to results. On each call, the wrapper checks if it's seen these arguments before. If yes, it returns the saved result without calling the original function</p><div data-node-type="callout" class="editor-callout"><div data-emoji="💡" data-node-type="callout-emoji" class="callout-emoji">💡</div><div data-node-type="callout-text" class="callout-text">This is the same concept behind Python's built-in <code>functools.lru_cache</code>. You just built a simplified version from scratch.</div></div>
</details>

* * *

## How Did you Do?

If you solved Exercises 1-6 without peeking, you've got a solid grasp of decorator fundamentals. If you also got 7-8, you understand decorators arguments. And if you nailed 9-10, you're ready to use decorators confidently in real projects.

The most common traps to watch for:

*   **Not using** `*args`, and `**kwargs` in the wrapper. It makes your decorator too rigid.
    
*   **Forgetting** `return result` in the wrapper. Your values silently become `None`.
    
*   **Putting** `return` **inside a loop** when you want the loop to complete. It exits on the first iteration.
    
*   **Printing instead of returning** from the wrapper. You values go to the screen instead of to the caller.
    

* * *

## Where to Go from Here

You now have a complete understanding of Python decorators. Some directions to explore next:

*   `functools.wraps`: always use it to preserve function metadata
    
*   **Class-based decorators**: using `__call__` to make objects behave like decorators
    
*   `functools.lru_cache`: the production-grade version of your Exercise 10
    
*   `@property`: a decorator that turns methods into computed attributes
    
*   **Framework decorators**: dig into how Flask's `@app.route()` or pytest's `@fixture` work internally
    

The foundation you've built here will make all of these feel approachable.

Thank you for reading 🙂.

* * *

> This is Part 4 of my Python Decorators series. The series builds one concept at a time, and each article assumes you've read the ones before it. Start from Part 1 if you haven't already.
