Lesson 58 of 70 – Python Decorators
83%

Python Decorators

A decorator is a function that allows you to modify or extend the behavior of another function without changing its original code.

Decorators are widely used in Python for logging, authentication, validation, timing, caching, and many other tasks.

Note: A decorator usually takes a function as an argument, adds some behavior, and returns a new function.
What is a Decorator?

A decorator is a function that wraps another function and changes or extends its behavior.

Python provides a special @ syntax for applying decorators.

def decorator(function):

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

    return wrapper


@decorator
def greet():
    print("Hello")


greet()
Output:
Before function
Hello
After function
Why Use Decorators?

Decorators are useful for:

  • Logging function calls.
  • Checking authentication.
  • Validating data.
  • Measuring execution time.
  • Adding permissions.
  • Caching results.
  • Adding reusable behavior to functions.
Functions are Objects

In Python, functions are objects. This means a function can be:

  • Stored in a variable.
  • Passed as an argument.
  • Returned from another function.
def greet():
    print("Hello")


message = greet

message()
Output:
Hello
Passing a Function as an Argument

A function can be passed to another function as an argument.

def greet():
    print("Hello")


def execute(function):
    function()


execute(greet)
Output:
Hello
Returning a Function

A function can also return another function.

def outer():

    def inner():
        print("Hello from inner function")

    return inner


function = outer()

function()
Output:
Hello from inner function
Creating a Simple Decorator
def decorator(function):

    def wrapper():

        print("Start")

        function()

        print("End")

    return wrapper


def greet():
    print("Hello")


greet = decorator(greet)

greet()
Output:
Start
Hello
End
Using @ Syntax

Python provides a shorter way to apply a decorator using the @ symbol.

def decorator(function):

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

    return wrapper


@decorator
def greet():
    print("Hello")


greet()
Output:
Before
Hello
After

The statement @decorator is essentially a convenient way of applying the decorator to the function.

Decorator with Arguments

If the decorated function accepts arguments, the wrapper should usually accept them as well.

def decorator(function):

    def wrapper(name):

        print("Before function")

        function(name)

        print("After function")

    return wrapper


@decorator
def greet(name):
    print("Hello", name)


greet("Rahul")
Output:
Before function
Hello Rahul
After function
Using *args and **kwargs

To create a decorator that works with functions having different arguments, use *args and **kwargs.

def decorator(function):

    def wrapper(*args, **kwargs):

        print("Function is running")

        return function(*args, **kwargs)

    return wrapper


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


print(add(10, 20))
Output:
Function is running
30
Decorator Returning a Value

A decorator should return the result of the original function when the result needs to be preserved.

def decorator(function):

    def wrapper(*args, **kwargs):

        result = function(*args, **kwargs)

        return result

    return wrapper


@decorator
def multiply(a, b):
    return a * b


print(multiply(5, 4))
Output:
20
Decorator for Logging

A decorator can be used to print information whenever a function is called.

def logger(function):

    def wrapper(*args, **kwargs):

        print("Calling function:", function.__name__)

        result = function(*args, **kwargs)

        return result

    return wrapper


@logger
def greet():
    print("Hello")


greet()
Output:
Calling function: greet
Hello
Decorator for Execution Time

Decorators can be used to measure how long a function takes to execute.

import time


def timer(function):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = function(*args, **kwargs)

        end = time.time()

        print("Time:", end - start)

        return result

    return wrapper


@timer
def calculate():

    total = 0

    for i in range(100000):
        total += i

    return total


calculate()

The decorator measures the approximate execution time of calculate().

Using functools.wraps

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

The functools.wraps decorator helps preserve that metadata.

from functools import wraps


def decorator(function):

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

        return function(*args, **kwargs)

    return wrapper


@decorator
def greet():
    """Display a greeting."""
    print("Hello")


print(greet.__name__)
print(greet.__doc__)
Output:
greet
Display a greeting.
Decorator for Authentication

Decorators can be used to check whether a user is allowed to execute a function.

def login_required(function):

    def wrapper(logged_in):

        if logged_in:
            return function()
        else:
            print("Please login first")

    return wrapper


@login_required
def dashboard():
    print("Welcome to dashboard")


dashboard(True)
dashboard(False)
Output:
Welcome to dashboard
Please login first
Decorator for Validation

A decorator can validate function arguments before calling the original function.

def positive_only(function):

    def wrapper(number):

        if number > 0:
            return function(number)

        print("Number must be positive")

    return wrapper


@positive_only
def square(number):
    print(number * number)


square(5)
square(-2)
Output:
25
Number must be positive
Multiple Decorators

More than one decorator can be applied to a function.

def decorator_one(function):

    def wrapper():
        print("Decorator One")
        function()

    return wrapper


def decorator_two(function):

    def wrapper():
        print("Decorator Two")
        function()

    return wrapper


@decorator_one
@decorator_two
def greet():
    print("Hello")


greet()
Output:
Decorator One
Decorator Two
Hello

Decorators are applied from the bottom upward in this example.

Decorator with Parameters

A decorator itself can also be configured using parameters. This requires another level of function nesting.

def repeat(times):

    def decorator(function):

        def wrapper():

            for i in range(times):
                function()

        return wrapper

    return decorator


@repeat(3)
def greet():
    print("Hello")


greet()
Output:
Hello
Hello
Hello
How a Decorator Works

Consider this code:

@decorator
def greet():
    print("Hello")

Python effectively performs an operation equivalent to:

def greet():
    print("Hello")


greet = decorator(greet)

The original function is passed to the decorator and the returned function becomes the new value of greet.

Decorator and Closure

Decorators commonly use nested functions. The inner wrapper function can access the function passed by the outer decorator.

def decorator(function):

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

    return wrapper

Here, wrapper() remembers the function provided to decorator(). This is related to Python's concept of closures.

Built-in Decorators

Python provides several decorators and decorator-like tools. Some commonly used examples are:

  • @property
  • @classmethod
  • @staticmethod
  • @abstractmethod
class Student:

    @staticmethod
    def school_name():
        return "ABC School"


print(Student.school_name())
Output:
ABC School
Common Uses of Decorators
Use Purpose
Logging Record function calls and related information.
Authentication Check whether a user has permission.
Validation Check input before executing a function.
Timing Measure execution time.
Caching Reuse previously calculated results.
Access Control Control access to functionality.
Complete Decorator Example
from functools import wraps


def log_function(function):

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

        print("Function started")

        result = function(*args, **kwargs)

        print("Function completed")

        return result

    return wrapper


@log_function
def add(a, b):

    return a + b


result = add(10, 20)

print("Result:", result)
Output:
Function started
Function completed
Result: 30
Advantages of Decorators
  • Promote code reuse.
  • Keep additional behavior separate from the main function.
  • Reduce duplicate code.
  • Useful for cross-cutting concerns such as logging and authentication.
  • Can modify function behavior without editing the original function.
  • Can be applied to many functions.
Key Points
  • A decorator modifies or extends the behavior of a function or other callable.
  • Decorators are commonly written using the @decorator syntax.
  • A decorator usually accepts a function and returns a wrapper function.
  • *args and **kwargs help decorators support flexible arguments.
  • functools.wraps helps preserve function metadata.
  • Multiple decorators can be applied to one function.
  • Decorators are useful for logging, authentication, validation, timing and caching.
  • Python provides built-in decorators such as @property, @classmethod and @staticmethod.

🧠 Quick Quiz

Question: Which symbol is used to apply a decorator to a function?