Decorators 101: The Basics

Decorators 101: The Basics – Gift Wrapping Your Python Functions! 🎁

Imagine you’re wrapping a gift. You take a plain box, add some shiny paper, a ribbon, and maybe a cute tag—transforming it into something more special. Decorators in Python do the same thing for functions! They take an existing function, tweak it, and return an enhanced version without changing the original code.

Whether you're a beginner or an intermediate Python developer, understanding decorators can level up your coding skills. Let’s break them down in a simple, friendly way.


1. What Are Decorators?

Decorators are functions that modify other functions. They let you add extra behavior (like logging, timing, or access control) to a function without rewriting it. Think of them as reusable "wrappers" that enhance your functions.

A Simple Decorator Example

def my_decorator(func):
    def wrapper():
        print("Something happens before the function runs.")  # Added behavior
        func()  # Original function runs here
        print("Something happens after the function runs.")   # Added behavior
    return wrapper

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

say_hello()

Output:

Something happens before the function runs.  
Hello!  
Something happens after the function runs.  

Here, @my_decorator modifies say_hello() by adding print statements before and after it runs.


2. Why Use Decorators?

Decorators help you:
Avoid repetitive code (DRY principle—Don’t Repeat Yourself!)
Separate concerns (keep logic clean and modular)
Extend functions without modifying their original code

Common Use Cases

  • Logging (track when a function runs)
  • Timing (measure how long a function takes)
  • Authorization (check permissions before running a function)
  • Caching (store results to avoid recomputation)

3. How Decorators Work Under the Hood

When you write:

@my_decorator  
def say_hello():  
    ...

Python translates it to:

say_hello = my_decorator(say_hello)  

This means:

  1. my_decorator takes say_hello as input.
  2. It returns a new function (wrapper) that includes extra steps.
  3. Now, say_hello points to the wrapped version!

4. Decorators with Arguments

What if your function has arguments? Use *args and **kwargs to handle them:

def smart_decorator(func):
    def wrapper(*args, **kwargs):
        print("Decorator: Before function")
        result = func(*args, **kwargs)  # Passes arguments to original func
        print("Decorator: After function")
        return result
    return wrapper

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

greet("Alice")

Output:

Decorator: Before function  
Hello, Alice!  
Decorator: After function  

Now, the decorator works with any function, no matter its arguments!


5. Real-World Example: Timing a Function

Want to measure how long a function takes? A decorator makes this easy:

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.2f} seconds.")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)  # Simulate a slow task

slow_function()  

Output:

slow_function took 2.00 seconds.  

Now you can reuse @timer on any function to track performance!


6. Chaining Multiple Decorators

You can apply multiple decorators to a single function:

def bold(func):
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold  
@italic  
def greet():  
    return "Hello!"  

print(greet())  

Output:

<b><i>Hello!</i></b>

The order matters! @bold wraps @italic, which wraps greet().


7. Fun Experiment: Try It Yourself!

Here’s a challenge:

  1. Create a decorator that logs a message before and after a function runs.
  2. Apply it to a function of your choice.
  3. Bonus: Modify it to work with functions that take arguments.

Example starter code:

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Starting {func.__name__}...")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}!")
        return result
    return wrapper

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

print(add(3, 5))

Final Thoughts

Decorators are like superhero capes for functions—they add powers without changing the core! Once you master them, you’ll see them everywhere in Python (Flask routes, @staticmethod, @property, etc.).

Your Turn:

  • Where could you use decorators in your code?
  • Can you think of a function that would benefit from logging or timing?

Drop your experiments in the comments—I’d love to see what you create! 🚀

Happy decorating! 🎀

What Are Python Decorators?