Fundamentals 20 min read

Introduction to Python Decorators: Concepts, Principles, and Practical Examples

Python decorators are a powerful syntactic feature that allow functions, methods, or classes to be wrapped and extended, enabling reusable code for logging, caching, authentication, and more, with detailed explanations of their underlying principles, manual implementations, syntax sugar, and usage with classes and built‑in decorators.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Introduction to Python Decorators: Concepts, Principles, and Practical Examples

Decorator Introduction

Decorators are an advanced Python syntax that can modify a function, method, or class. They provide a simple, readable way to add cross‑cutting concerns such as logging, performance testing, transaction handling, web permission checks, or caching.

The advantage of decorators is that they can extract repetitive code unrelated to the core functionality of many functions and reuse it. In other words, a decorator can "decorate" a function with completely different behavior, effectively orthogonalizing business logic.

Python decorators are syntactically similar to Java/C# annotations (e.g., @XXX) but are far simpler because they do not require learning a separate annotation library. They are a functional‑programming technique built directly into the language.

How Decorators Work

In Python, implementing a decorator is straightforward because functions are first‑class objects.

Functions Are Objects

To understand decorators, you must first know that functions in Python are objects. The following example shows that a function can be assigned to another variable, passed around, and even deleted while the new name still works.

def shout(word="yes"):
    return word.capitalize() + "!"

print(shout())  # outputs: 'Yes!'

# Assign the function to another variable
scream = shout
print(scream())  # outputs: 'Yes!'

# Delete the original name
del shout
try:
    print(shout())
except NameError as e:
    print(e)  # outputs: "name 'shout' is not defined"
print(scream())  # still works, outputs: 'Yes!'

Functions can also be defined inside other functions, which enables returning a function from another function.

def talk():
    def whisper(word="yes"):
        return word.lower() + "..."
    print(whisper())

talk()  # outputs: "yes..."

# The inner function is not accessible outside
try:
    print(whisper())
except NameError as e:
    print(e)  # outputs: "name 'whisper' is not defined"

Function References

Because functions are objects, they can be returned from other functions:

def get_talk(type="shout"):
    def shout(word="yes"):
        return word.capitalize() + "!"
    def whisper(word="yes"):
        return word.lower() + "..."
    if type == "shout":
        return shout  # return the function object, not the result
    else:
        return whisper

talk = get_talk()
print(talk)          # <function shout at 0x...>
print(talk())        # outputs: Yes!
print(get_talk("whisper")())  # outputs: yes...

Functions can also be passed as arguments:

def shout(word="yes"):
    return word.capitalize() + "!"

scream = shout

def do_something_before(func):
    print("I do something before then I call the function you gave me")
    print(func())

do_something_before(scream)
# outputs:
# I do something before then I call the function you gave me
# Yes!

Decorator in Practice

Now that the basics are clear, decorators can be used to wrap additional code before or after a function runs without modifying the original function.

Manually Creating a Decorator

# A decorator is a function that takes another function as an argument

def my_shiny_new_decorator(a_function_to_decorate):
    # Define the wrapper that will run before and after the original function
    def the_wrapper_around_the_original_function():
        print("Before the function runs")
        a_function_to_decorate()
        print("After the function runs")
    # Return the wrapper so it can be called later
    return the_wrapper_around_the_original_function

# Example usage

def a_stand_alone_function():
    print("I am a stand alone function, don't you dare modify me")

# Decorate the function
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)

a_stand_alone_function_decorated()
# outputs:
# Before the function runs
# I am a stand alone function, don't you dare modify me
# After the function runs

Decorator Syntax Sugar

Using the @ syntax makes the previous example more concise:

def my_shiny_new_decorator(a_function_to_decorate):
    def the_wrapper_around_the_original_function():
        print("Before the function runs")
        a_function_to_decorate()
        print("After the function runs")
    return the_wrapper_around_the_original_function

@my_shiny_new_decorator
def another_stand_alone_function():
    print("Leave me alone")

another_stand_alone_function()
# outputs:
# Before the function runs
# Leave me alone
# After the function runs

Applying multiple decorators stacks them, with the decorator closest to the function being applied first.

def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    return wrapper

@ingredients
@bread
def sandwich(food="--ham--"):
    print(food)

sandwich()
# outputs:
# </''''''\>
# #tomatoes#
# --ham--
# ~salad~
# <\______/>

Passing Arguments to Decorators

When a decorator needs its own parameters, it must return a function that returns the actual wrapper.

def pre_str(pre=''):
    def decorator(F):
        def new_F(a, b):
            print(pre + " input", a, b)
            return F(a, b)
        return new_F
    return decorator

@pre_str('^_^')
def square_sum(a, b):
    return a**2 + b**2

@pre_str('T_T')
def square_diff(a, b):
    return a**2 - b**2

print(square_sum(3, 4))   # prints: ('^_^ input', 3, 4) and then 25
print(square_diff(3, 4))  # prints: ('T_T input', 3, 4) and then -7

Decorating Methods and Classes

Because methods are just functions that receive the instance as the first argument (self), the same decorator pattern works for them.

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3  # make the age appear younger
        return method_to_decorate(self, lie)
    return wrapper

class Lucy(object):
    def __init__(self):
        self.age = 32

    @method_friendly_decorator
    def say_your_age(self, lie):
        print("I am %s, what did you think?" % (self.age + lie))

l = Lucy()
l.say_your_age(-3)  # outputs: I am 26, what did you think?

A generic decorator that accepts any arguments can be written with *args and **kwargs.

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print("Do I have args?:")
        print(args)
        print(kwargs)
        return function_to_decorate(*args, **kwargs)
    return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print("Python is cool, no argument here.")

function_with_no_argument()
# outputs:
# Do I have args?:
# ()
# {}
# Python is cool, no argument here.

Class Decorators

Since Python 2.6, decorators can also wrap entire classes.

def decorator(aClass):
    class newClass:
        def __init__(self, age):
            self.total_display = 0
            self.wrapped = aClass(age)
        def display(self):
            self.total_display += 1
            print("total display", self.total_display)
            self.wrapped.display()
    return newClass

@decorator
class Bird:
    def __init__(self, age):
        self.age = age
    def display(self):
        print("My age is", self.age)

eagleLord = Bird(5)
for i in range(3):
    eagleLord.display()
# each call prints the total display count and the bird's age

Built‑in Decorators

Python provides three commonly used built‑in decorators for class members: property, staticmethod, and classmethod.

class XiaoMing:
    first_name = '明'
    last_name = '小'

    @property
    def full_name(self):
        return self.last_name + self.first_name

xiaoming = XiaoMing()
print(xiaoming.full_name)  # accesses the method like an attribute
class XiaoMing:
    @staticmethod
    def say_hello():
        print('同学你好')

XiaoMing.say_hello()
# also works on an instance
xiaoming = XiaoMing()
xiaoming.say_hello()
class XiaoMing:
    name = '小明'

    @classmethod
    def say_hello(cls):
        print('同学你好,我是' + cls.name)
        print(cls)

XiaoMing.say_hello()

Preserving Function Metadata with functools.wraps

When a decorator returns a wrapper function, the original function’s name and docstring are lost. Using functools.wraps copies these attributes back to the wrapper.

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('123')
        return func(*args, **kwargs)
    return wrapper

@decorator
def say_hello():
    """doc of say hello"""
    print('同学你好')

print(say_hello.__name__)  # still 'say_hello'
print(say_hello.__doc__)   # still 'doc of say hello'

Summary

Decorators are a core Python feature that enable name binding and code reuse. While they are not the same as the object‑oriented Decorator Pattern, they provide a concise, readable way to extend functionality such as logging, caching, authentication, and more.

Understanding decorators is valuable for any Python developer because they appear frequently in real‑world projects and libraries.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonprogrammingTutorialDecoratorfunction
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.