PyLearn Learn Python Professionally
Home / Lessons / Python Decorators
Lesson 52 · Advanced Python Concepts

Python Decorators

Learn how decorators modify or extend the behavior of functions without changing their original source code. Build reusable logging, timing, authentication, and access-control decorators.

50–60 minutes Intermediate Advanced Python Concepts
0%
01

Learning Objectives

By the end of this lesson, you will understand how Python decorators work and how they can be used to extend the behavior of existing functions.

You will learn how to:

  • Understand functions as first-class objects.
  • Pass functions to other functions.
  • Return functions from functions.
  • Create basic decorators.
  • Use the @decorator syntax.
  • Handle function arguments with *args and **kwargs.
  • Preserve function metadata with functools.wraps.
  • Create logging and timing decorators.
  • Build authentication and access-control decorators.
  • Use multiple decorators together.
  • Create decorators that accept their own parameters.
02

What Are Decorators?

A decorator is a function that takes another function, adds or modifies behavior, and returns a new function.

In simple terms, a decorator allows you to say:

"Before or after this function runs, I want to perform some additional work."

The original function does not need to be rewritten. The decorator wraps it with additional behavior.

def decorator(function):

    def wrapper():
        # additional behavior
        function()

    return wrapper

This pattern is extremely useful when the same additional behavior needs to be applied to many functions.

03

Why Decorators Matter

Imagine you have 20 functions and every function needs logging before it runs.

You could manually add logging code to every function, but that creates repetition.

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


def subtract(a, b):
    print("Calling subtract")
    return a - b


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

A decorator lets you separate the logging logic from the business logic.

Common uses of decorators

  • Logging
  • Performance measurement
  • Authentication
  • Authorization
  • Caching
  • Validation
  • Retry logic
  • Access control
  • Monitoring
04

Functions Are Objects

To understand decorators, you first need to understand that Python functions are objects.

This means a function can be stored in a variable, passed to another function, and returned from a function.

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


print(greet)

Python stores the function object in memory, and the variable greet refers to that object.

Output

<function greet at 0x...>
05

Assigning Functions to Variables

Because functions are objects, another variable can refer to the same function.

def greet():
    print("Hello, Python!")


message = greet

message()

Output

Hello, Python!

Both greet and message refer to the same function.

06

Passing Functions as Arguments

A function can receive another function as an argument.

def greet():
    return "Hello!"


def execute(function):
    result = function()
    print(result)


execute(greet)

Output

Hello!

Notice that we pass greet, not greet().

Important:

greet refers to the function itself, while greet() calls the function immediately.

07

Returning Functions

Functions can also return other functions.

def outer():

    def inner():
        print("Inside inner function")

    return inner


function = outer()

function()

Output

Inside inner function

This ability to create, pass, and return functions is the foundation of decorators.

08

Nested Functions

A nested function is a function defined inside another function.

def outer():

    print("Outer function")

    def inner():
        print("Inner function")

    inner()


outer()

Output

Outer function
Inner function

Decorators commonly use nested functions because the inner function becomes the wrapper around the original function.

09

Building a Basic Decorator

Let's create our first real decorator.

def my_decorator(func):

    def wrapper():
        print("Before function")
        func()
        print("After function")

    return wrapper


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


decorated = my_decorator(greet)

decorated()

Output

Before function
Hello!
After function

The decorator receives greet, creates a wrapper, and returns that wrapper.

10

The @ Decorator Syntax

Python provides a cleaner syntax for applying decorators.

def my_decorator(func):

    def wrapper():
        print("Before function")
        func()
        print("After function")

    return wrapper


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


greet()

This:

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

is conceptually equivalent to:

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


greet = my_decorator(greet)
The @ syntax is simply a convenient way to apply a decorator.
11

Decorators with Function Arguments

Real functions usually receive arguments. Therefore, our wrapper must be able to receive them too.

def logger(func):

    def wrapper(*args, **kwargs):

        print("Function:", func.__name__)
        print("Arguments:", args)
        print("Keyword arguments:", kwargs)

        return func(*args, **kwargs)

    return wrapper


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


print(add(10, 20))

Output

Function: add
Arguments: (10, 20)
Keyword arguments: {}
30
12

Using *args and **kwargs

Decorators often need to work with functions that have different numbers and types of arguments.

That's why decorators commonly use:

  • *args for positional arguments
  • **kwargs for keyword arguments
def decorator(func):

    def wrapper(*args, **kwargs):

        print("Arguments received")

        return func(*args, **kwargs)

    return wrapper

This makes the decorator flexible enough to wrap many different functions.

13

Returning the Wrapped Function's Result

A common decorator mistake is forgetting to return the original function's result.

The correct pattern is:

def decorator(func):

    def wrapper(*args, **kwargs):

        result = func(*args, **kwargs)

        return result

    return wrapper

You can also write:

return func(*args, **kwargs)
Common mistake:

If the wrapped function returns a value and the decorator does not return it, the caller may receive None.

14

functools.wraps

When a decorator replaces a function with a wrapper, metadata such as the original function's name and documentation can be lost.

Python provides functools.wraps to solve this problem.

from functools import wraps


def logger(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        print("Calling:", func.__name__)

        return func(*args, **kwargs)

    return wrapper

Now Python can preserve important metadata from the original function.

Why use @wraps?

  • Preserves the function name.
  • Preserves the function docstring.
  • Improves debugging.
  • Improves introspection.
  • Makes decorated functions behave more naturally.
15

Practical Decorator — Logging

Logging is one of the most common real-world uses of decorators.

from functools import wraps


def logger(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        print("Function:", func.__name__)
        print("Arguments:", args)
        print("Keyword arguments:", kwargs)

        return func(*args, **kwargs)

    return wrapper


@logger
def calculate_total(price, quantity):

    return price * quantity


print(calculate_total(500, 3))

Output

Function: calculate_total
Arguments: (500, 3)
Keyword arguments: {}
1500

The calculation itself remains clean while logging is handled separately.

16

Practical Decorator — Measuring Execution Time

Performance measurement is another excellent use case for decorators.

import time
from functools import wraps


def timer(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        start = time.perf_counter()

        result = func(*args, **kwargs)

        end = time.perf_counter()

        print(
            f"{func.__name__} took "
            f"{end - start:.6f} seconds"
        )

        return result

    return wrapper


@timer
def process_data():

    total = 0

    for number in range(1_000_000):
        total += number

    return total


print(process_data())

The decorator measures execution time without changing the main logic inside process_data().

Tip:

time.perf_counter() is designed for measuring elapsed time and is generally preferable to ordinary wall-clock timestamps for performance measurements.

17

Practical Decorator — Authentication

Decorators can also check whether a user is allowed to perform a particular operation.

from functools import wraps


def require_admin(func):

    @wraps(func)
    def wrapper(user, *args, **kwargs):

        if user.get("role") != "admin":
            print("Access denied.")
            return None

        return func(user, *args, **kwargs)

    return wrapper


@require_admin
def delete_user(user, username):

    print(f"Deleting {username}")


admin = {
    "name": "Ali",
    "role": "admin"
}

customer = {
    "name": "Sara",
    "role": "customer"
}


delete_user(admin, "Ahmed")
delete_user(customer, "Ahmed")

Output

Deleting Ahmed
Access denied.

In production applications, authentication and authorization should be implemented using the application's proper security framework. This example demonstrates the decorator pattern itself.

18

Using Multiple Decorators

A function can have more than one decorator.

@decorator_one
@decorator_two
def greet():

    print("Hello!")

Decorators are applied from the bottom upward.

Conceptually, this is similar to:

greet = decorator_one(
    decorator_two(greet)
)
Order matters.

Changing the order of decorators can change the behavior of the program.

19

Decorators with Their Own Parameters

Sometimes we want to configure a decorator.

For example, we might want a decorator that executes a function three times.

from functools import wraps


def repeat(times):

    def decorator(func):

        @wraps(func)
        def wrapper(*args, **kwargs):

            result = None

            for _ in range(times):
                result = func(*args, **kwargs)

            return result

        return wrapper

    return decorator


@repeat(3)
def greet():

    print("Hello!")


greet()

Output

Hello!
Hello!
Hello!

Notice that this requires three levels:

  1. The decorator configuration function.
  2. The actual decorator.
  3. The wrapper around the original function.

This pattern is commonly called a decorator factory.

20

Built-in Python Decorators

Python itself provides several useful decorators.

@staticmethod

class MathTools:

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


print(MathTools.add(10, 20))

A static method does not require an instance or self.

@classmethod

class Product:

    category = "Electronics"

    @classmethod
    def get_category(cls):
        return cls.category


print(Product.get_category())

A class method receives the class as cls.

@property

class Circle:

    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2


circle = Circle(5)

print(circle.area)

The @property decorator allows a method to be accessed like an attribute.

Common built-in decorators

  • @staticmethod
  • @classmethod
  • @property
  • @functools.wraps
21

Common Decorator Mistakes

Mistake 1 — Forgetting to return the wrapper

def decorator(func):

    def wrapper():
        func()

    # Missing return wrapper

The decorator must normally return the wrapper.

Mistake 2 — Forgetting function arguments

def decorator(func):

    def wrapper():
        return func()

    return wrapper

This only works for functions that take no arguments.

A more flexible approach is:

def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

Mistake 3 — Forgetting the return value

If the original function returns a value, the wrapper should normally return that value.

Mistake 4 — Not using functools.wraps

Without @wraps, useful metadata from the original function can be lost.

Mistake 5 — Overusing decorators

Decorators are powerful, but too many layers can make code difficult to understand.

Professional rule:

Use decorators when they make a repeated, cross-cutting behavior cleaner and more reusable.

22

Practice Exercises

Exercise 1 — Greeting Decorator

Create a decorator that prints "Starting..." before a function and "Finished!" after it.

Exercise 2 — Logging

Create a decorator that prints the function name whenever the function is called.

Exercise 3 — Calculator

Create a decorator that prints the arguments passed to a calculator function.

Exercise 4 — Timing

Build a decorator that measures how long a function takes to execute.

Exercise 5 — Role Checking

Create a decorator that allows a function to run only when the supplied user has an admin role.

23

Mini Challenge — Performance & Access Logger

Build a reusable decorator system for a small ecommerce application.

Your requirements

  1. Create a decorator that logs the function name.
  2. Log positional and keyword arguments.
  3. Measure execution time.
  4. Return the original function's result.
  5. Preserve function metadata using @wraps.
  6. Create an optional role-checking decorator.
  7. Apply multiple decorators to an ecommerce function.

Start with a function such as:

def calculate_order_total(items):

    total = 0

    for item in items:
        total += item["price"] * item["quantity"]

    return total

Then create decorators such as:

@logger
@timer
def calculate_order_total(items):

    total = 0

    for item in items:
        total += item["price"] * item["quantity"]

    return total

Finally, experiment with the order of the decorators and observe how the execution flow changes.

Challenge Goal

Build a small reusable decorator toolkit that could realistically be used in a Python business application.

24

Knowledge Check

Test your understanding of Python decorators.

1. What is a decorator?

2. Which syntax applies a decorator?

3. Why are *args and **kwargs commonly used in decorators?

4. What does functools.wraps help preserve?

5. What happens when multiple decorators are stacked?

6. Which decorator is commonly used to create a computed property?

7. What should a wrapper usually do when the original function returns a value?

8. What does @staticmethod create?

9. What is a decorator factory?

10. Why are decorators useful in professional applications?

25

Lesson Summary

Decorators are one of Python's most powerful tools for creating reusable behavior around functions and methods.

Key Concepts

  • Functions are first-class objects in Python.
  • Functions can be passed as arguments.
  • Functions can return other functions.
  • Decorators wrap existing functions.
  • The @decorator syntax provides a clean way to apply decorators.
  • *args and **kwargs make decorators flexible.
  • functools.wraps preserves function metadata.
  • Decorators are useful for logging, timing, validation, authentication, and monitoring.
  • Multiple decorators can be stacked.
  • Decorator order can affect program behavior.
  • Decorator factories allow decorators to receive configuration parameters.

The next lesson moves from decorators into another powerful Python tool: Regular Expressions. You will learn how to search, validate, extract, and transform text using patterns.

Finished this lesson?

Mark this lesson complete when you are comfortable creating and using decorators.