# Understanding Python Decorators From Scratch

If you've ever seen the `@` symbol sitting on top of a Python function and wondered what sorcery that is, this article is for you.

Decorators are one of those Python concepts that look intimidating but are built on ideas you already know. The problem is that most tutorials jump straight to the syntax without building the mental model first. So you end up memorizing the pattern without truly understanding **why** it works.

Let's fix that. We're going to build up to decorators one concept at a time, and by the end, you'll be writing your own with confidence

* * *

## Functions Are Objects: That Changes Everything

Here is the thing you need to internalize: **in Python, a function is just an object**. Like a string, like a number, like a list, you can store it in a variable, pass it around, and do whatever you want with it.

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

# Store the function in another variable (no parentheses!)
my_function = say_hello

# Both names point to the same function
say_hello() # Hello!
my_function() # Hello!
```

Notice something critical here: `say_hello` without parentheses is the function **itself**, the object. `say_hello()` with parentheses **calls** the function and gives you the result. This distinction matters for everything that follows.

Think of a recipe card. `say_hello` is the card itself. You can hand it to someone, photocopy it, pin it on a wall. `say_hello()` is you actually cooking the recipe.

* * *

## You Can Pass a Function to Another Function

Since functions are objects, you can hand one to another function as an argument, just like you'd pass a string or a number.

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

def say_goodbye():
    print("Goodbye!")

def call_twice(func):
    func()
    func()

call_twice(say_hello)
# Hello!
# Hello!

call_twice(say_goodbye)
# Goodbye!
# Goodbye!
```

`call_twice` doesn't know or care about which function you give it. It just calls whatever arrives, twice. We're passing `say_hello` (the object), not `say_hello()` (the result).

This might feel strange if you're coming from another language, but it's one of Python's superpowers. Functions are first-class citizens. They can go anywhere any other value can go.

* * *

## A Function Can Create and Return Another Function

This where things get interesting. A function can **build** a new function inside itself and give it back to you:

```python
def make_greeter(name):
    def greeter():
        print(f"Hello, {name}!")
    return greeter

greet_moussa = make_greeter("Moussa")
greet_kalam = make_greeter("Kalam")

greet_moussa()  # Hello, Moussa!
greet_kalam()   # Hello, Kalam!
```

Each call to `make_greeter` creates a brand-new function that remembers the name it was created with. The inner function `greeter` holds onto the `name` variable even after `make_greeter` has finished executing.

This pattern is called **closure**, a function that remembers values from the scope where it was created. Closures are the engine that powers decorators, so make sure this concept clicks before moving on.

* * *

## Combining the Pieces: Take a Function, Return a New One

Now let's combine everything. What if we write a function that **takes a function as input** and **returns a new function as output**?

```python
def make_louder(func):
    def new_function():
        print("I'M ABOUT TO DO SOMETHING!")
        func()
        print("I'M DONE!")
    return new_function

def say_hello():
    print("Hello!")

louder_hello = make_louder(say_hello)

# The original still works normally
say_hello()
# Hello!

# The new version adds extra behavior
louder_hello()
# I'M ABOUT TO DO SOMETHING!
# Hello!
# I'M DONE!
```

Please read that carefully! `make_louder` received `say_hello`, wrapped it in extra behavior (printing before and after), and returned a brand new function. The original `say_hello` is untouched.

**This is the entire idea behind decorators**. Everything else is just syntax and polish.

* * *

## Replacing the Original

Instead of keeping both the old and new versions, what if we replace `say_hello` with the wrapped version?

```python
say_hello = make_louder(say_hello)

say_hello()
# I'M ABOUT TO DO SOMETHING!
# Hello!
# I'M DONE!
```

Look at this line: `say_hello = make_louder(say_hello)`. We're saying: "take the old `say_hello`, wrap it and store the result back as `say_hello`."

Now whenever anyone calls `say_hello()`, they get the enhanced the version. And that one line is so common in Python that the language gives us a shortcut for it.

* * *

## The `@` Symbol Is Just a Shortcut

```python
def make_louder(func):
    def new_function():
        print("I'M ABOUT TO DO SOMETHING!")
        func()
        print("I'M DONE!")
    return new_function

@make_louder
def say_hello():
    print("Hello!")

say_hello()
# I'M ABOUT TO DO SOMETHING!
# Hello!
# I'M DONE!
```

`@make_louder` on top of `say_hello` is **exactly identical** to writing `say_hello = make_louder(say_hello)` after the function definition. It's just cleaner syntax, what Python calls "syntactic sugar".

That's literally all the `@` symbol does. No magic. No special mechanism. Just a shortcut for a pattern we already understand.

`make_louder` is a **decorator.**

* * *

## The Arguments Problem

There's a catch with our current approach. What if the decorated function takes arguments?

```python
def log_calls(func):
    def wrapper():          # takes NO arguments!
        print(f"Calling {func.__name__}")
        func()              # passes NO arguments!
    return wrapper

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

add(2, 3)  # TypeError: wrapper() takes 0 arguments but 2 were given
```

It breaks because `add(2, 3)` now actually calls `wrapper(2, 3)`, but `wrapper` doesn't accept any arguments.

The fix is `*args` and `**kwargs`.  
You've probably seen these before, but here's what they actually do:

*   `*args` lets a function accept any number of positional arguments. It packs them into a tuple.
    
*   `**kwargs` does the same for keyword arguments. It packs them into a dictionary.
    

You can read more `*args` and `**kwargs` in [this article I wrote](https://blog.moussaamzat.dev/python-args-kwargs-complete-guide).

Put them together in the wrapper, and it can accept literally **any** combination of arguments and pass them straight through to the original function. It doesn't need to know what the function expects. It just forwards everything.

```python
def log_calls(func):
    def wrapper(*args, **kwargs):         # accept anything
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)    # forward everything
        return result                     # return the result
    return wrapper

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

@log_calls
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(add(2, 3))                          # Calling add → 5
print(greet("Moussa", greeting="Hey"))    # Calling greet → Hey, Moussa!
```

By using `*args` and `**kwargs`, the wrapper doesn't need to know anything about the original function's signature. It catches everything and forwards it blindly. This is what makes the decorator generic, working with **any** function.

* * *

## A Practical Example: Logging

Let's see why this pattern is actually useful. Imagine you want to log every time certain functions are called:

```python
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f">>> Calling {func.__name__}()")
        result = func(*args, **kwargs)
        print(f">>> {func.__name__}() finished")
        return result
    return wrapper

@log_calls
def process_payment():
    print("Payment processed!")

@log_calls
def send_email():
    print("Email sent!")

@log_calls
def update_database():
    print("Database updated!")
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Call each of the decorated functions, and analyze the results.</div>
</div>

You wrote the logging logic **once** and applied it to three functions. Without decorators, you'd have to copy and paste those `print` statement into every single function. And when you want to change the log format, you change it in one place instead of hunting through your entire codebase.

* * *

## The Universal Template

Every decorator follows the same recipe:

```python
def my_decorator(func):            # 1. Take a function in
    def wrapper(*args, **kwargs):   # 2. Create a new function
        # do something before
        result = func(*args, **kwargs)  # 3. Call the original
        # do something after
        return result               # 4. Return the result
    return wrapper                  # 5. Return the wrapper

@my_decorator                      # 6. Replace the original
def my_function():
    pass
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">That's the pattern. Memorize it now that you understand the idea behind it. Once this clicks, you can write any decorator by just filling in the <strong>before </strong>and <strong>after</strong> parts.</div>
</div>

* * *

## One Last Best Practice: `functools.wraps`

When you wrap a function, the wrapper replaces the original's metadata: its name, docstring, and other attributes. This can cause confusion during debugging.

```python
@log_calls
def add(a, b):
    """Add two numbers.""" # Here is the docstring
    return a + b

print(add.__name__)  # "wrapper" — not "add"!
print(add.__doc__)  # None — the docstring is gone! 
```

Fix this with `functools.wraps`

```python
from functools import wraps


def log_calls(func):
    @wraps(func)  # preserves func's name, docstring, etc.
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        return result
    return wrapper


@log_calls
def add(a, b):
    """Add two numbers.""" # Here is the docstring
    return a + b

print(add.__name__)  # add
print(add.__doc__)  # Add two numbers.
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I'd advise you always use <code>@wraps</code>. It's a one-line addition that prevents real headaches down the road.</div>
</div>

* * *

## What's Next?

You now understand the complete foundation of Python decorators. In the next article, we'll tackle a subtle but critical trap that catches almost every developer learning decorators: the difference between `print` and `return` inside wrappers, and why confusing them makes your values silently disappear.

* * *

> *This is Part 1 of my Python Decorators series. Follow along for the next parts where we cover the* `print` *vs* `return` *trap, decorators with arguments, stacking, and practice exercises.*
