Skip to main content

Decorators (Part 1): Introduction to Decorators

Welcome to the final topic in our series on advanced Python concepts: decorators. Decorators are one of the most powerful and widely used features in Python, especially in web frameworks like Flask and Django. They might seem magical at first, but they are built directly on top of concepts we already know.

A decorator is a design pattern that allows you to add new functionality to an existing object (like a function or method) without modifying its source code.


📚 Prerequisites

To understand decorators, you must first grasp a key Python concept: functions are first-class objects. This means that in Python, a function is just another object, like an integer or a list. You can:

  1. Assign a function to a variable.
  2. Pass a function as an argument to another function.
  3. Return a function from another function.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Foundation: Understand that functions are first-class objects.
  • What a Decorator Is: See that a decorator is essentially a function that takes another function as an argument, adds functionality, and returns a new function.
  • The Manual Way: How to manually apply a decorator to a function.
  • The @ Syntax: How to use Python's elegant "syntactic sugar" for applying decorators.

🧠 Section 1: The Foundation - Functions as Objects

Let's quickly review what it means for functions to be first-class objects.

def say_hello(name):
return f"Hello, {name}!"

# 1. You can assign a function to a variable
greet = say_hello
print(greet("Alice")) # Output: Hello, Alice!

# 2. You can pass a function as an argument
def process_greeting(greeter_func, name):
# Call the function that was passed in
print(greeter_func(name))

process_greeting(say_hello, "Bob") # Output: Hello, Bob!

This ability to pass functions around is the key to how decorators work.


💻 Section 2: Building Your First Decorator

A decorator is a function that wraps another function. It takes a function as an argument, defines a new "wrapper" function inside it that adds some behavior, and returns the wrapper function.

Let's create a simple decorator that logs when a function is about to be executed.

# decorator_manual.py

# This is our decorator
def logger_decorator(func_to_wrap):
"""A simple decorator that logs a message."""

# This is the new function that will be returned
def wrapper():
print(f"About to run the function: {func_to_wrap.__name__}")
# Call the original function that was passed in
func_to_wrap()
print("Finished running the function.")

# The decorator returns the wrapper function
return wrapper

# This is the function we want to decorate
def stand_alone_function():
print("I am a standalone function.")


# --- The Manual Decoration Process ---

# 1. Pass our original function to the decorator
wrapped_function = logger_decorator(stand_alone_function)

# 2. The decorator returns a new function ('wrapper') which we store
# in the 'wrapped_function' variable.

# 3. Call the new wrapped function
wrapped_function()

Output:

About to run the function: stand_alone_function
I am a standalone function.
Finished running the function.

We have successfully "decorated" our original function, adding logging behavior before and after it runs without ever changing the source code of stand_alone_function.


🛠️ Section 3: The @ Syntax - Syntactic Sugar

The manual process above is a bit verbose. Python provides a much cleaner, more readable syntax for applying decorators: the @ symbol.

The @ syntax is placed directly above the function definition.

Let's rewrite the previous example using this "syntactic sugar."

# decorator_syntax_sugar.py

def logger_decorator(func_to_wrap):
def wrapper():
print(f"About to run the function: {func_to_wrap.__name__}")
func_to_wrap()
print("Finished running the function.")
return wrapper


# The '@' symbol applies the decorator automatically
@logger_decorator
def stand_alone_function():
print("I am a standalone function.")


# Now, just call the function as you normally would
stand_alone_function()

This code is exactly equivalent to the manual example. The line @logger_decorator is just a shortcut for writing stand_alone_function = logger_decorator(stand_alone_function) right after the function is defined.

This is the standard and preferred way to use decorators in Python.


✨ Conclusion & Key Takeaways

You've just learned the fundamental theory behind decorators. They are a powerful form of metaprogramming that allows you to modify or enhance functions and methods in a clean, reusable way.

Let's summarize the key takeaways:

  • Functions are Objects: The fact that functions can be passed as arguments and returned from other functions is what makes decorators possible.
  • A Decorator is a Wrapper: A decorator is a function that takes another function as input, defines a new wrapper function that adds functionality, and returns the wrapper.
  • @ is Syntactic Sugar: The @my_decorator syntax is the Pythonic way to apply a decorator to a function. It's a shortcut for my_function = my_decorator(my_function).

Challenge Yourself: Create a decorator called shout. When you apply it to a function that returns a string, the shout decorator should take that string and return it in all uppercase with an exclamation mark at the end.

@shout
def greet():
return "hello there"

print(greet()) # Should print "HELLO THERE!"

➡️ Next Steps

Our first decorator was simple and only worked for functions that take no arguments. What happens when we need to decorate a function that has parameters? In the next article, "Decorators (Part 2): Decorators with arguments," we'll learn how to use *args and **kwargs to create decorators that can wrap any function.

Happy decorating!