Fundamentals 20 min read

Understanding Python Decorators: Concepts, Usage, and Advanced Examples

This article explains the fundamentals of Python decorators, illustrating their purpose, underlying mechanics, and practical applications through numerous code examples ranging from simple hello‑world wrappers to class‑based, parameterized, and asynchronous decorators, while also discussing side‑effects and best‑practice solutions.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Understanding Python Decorators: Concepts, Usage, and Advanced Examples

Python decorators are a syntactic feature that allows a function to be wrapped by another function, enabling additional behavior without modifying the original function's code. Unlike the Decorator design pattern in object‑oriented programming, Python decorators rely on higher‑order functions and first‑class functions.

Hello World Example

A minimal decorator that prints messages before and after calling the wrapped function:

def hello(fn):
    def wrapper():
        print("hello, %s" % fn.__name__)
        fn()
        print("goodby, %s" % fn.__name__)
    return wrapper

@hello
def foo():
    print("i am foo")

foo()

Running this script outputs:

hello, foo
i am foo
goodby, foo

The interpreter rewrites the @decorator syntax as func = decorator(func) , meaning the original function is replaced by the wrapper returned by the decorator.

Essence of a Decorator

A decorator receives a function object, returns a new function (often called wrapper ) that may call the original function, and the assignment func = decorator(func) binds the name to the wrapper.

Multiple and Parameterized Decorators

Multiple decorators stack from the bottom up:

@decorator_one
@decorator_two
def func():
    pass

which is equivalent to func = decorator_one(decorator_two(func)) . A decorator with arguments must return a real decorator:

@decorator(arg1, arg2)
def func():
    pass

is interpreted as func = decorator(arg1, arg2)(func) .

Class‑Based Decorators

Decorators can be implemented as callable classes that store the target function in __init__ and execute it in __call__ :

class myDecorator(object):
    def __init__(self, fn):
        print("inside myDecorator.__init__()")
        self.fn = fn
    def __call__(self):
        self.fn()
        print("inside myDecorator.__call__()")

@myDecorator
def aFunction():
    print("inside aFunction()")

print("Finished decorating aFunction()")
aFunction()

Using Decorators to Inject Call Parameters

Three common patterns are shown:

Injecting a keyword argument via **kwargs :

def decorate_A(function):
    def wrap_function(*args, **kwargs):
        kwargs['str'] = 'Hello!'
        return function(*args, **kwargs)
    return wrap_function

@decorate_A
def print_message_A(*args, **kwargs):
    print(kwargs['str'])

print_message_A()

Injecting a positional argument directly:

def decorate_B(function):
    def wrap_function(*args, **kwargs):
        str = 'Hello!'
        return function(str, *args, **kwargs)
    return wrap_function

@decorate_B
def print_message_B(str, *args, **kwargs):
    print(str)

print_message_B()

Injecting via *args manipulation:

def decorate_C(function):
    def wrap_function(*args, **kwargs):
        str = 'Hello!'
        args = args + (str,)
        return function(*args, **kwargs)
    return wrap_function

class Printer:
    @decorate_C
    def print_message(self, str, *args, **kwargs):
        print(str)

p = Printer()
p.print_message()

Side Effects and functools.wraps

Decorated functions lose their original metadata (e.g., __name__ becomes wrapper ). Using functools.wraps preserves the original name and docstring:

from functools import wraps

def hello(fn):
    @wraps(fn)
    def wrapper():
        print("hello, %s" % fn.__name__)
        fn()
        print("goodby, %s" % fn.__name__)
    return wrapper

@hello
def foo():
    """foo help doc"""
    print("i am foo")

foo()
print(foo.__name__)   # prints foo
print(foo.__doc__)    # prints foo help doc

Even with wraps , introspection tools like inspect.getargspec may still miss arguments; a custom get_true_argspec using closure inspection can recover them.

Various Practical Decorator Examples

Memoization (caching) for a recursive Fibonacci function:

from functools import wraps

def memo(fn):
    cache = {}
    miss = object()
    @wraps(fn)
    def wrapper(*args):
        result = cache.get(args, miss)
        if result is miss:
            result = fn(*args)
            cache[args] = result
        return result
    return wrapper

@memo
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

Profiler decorator using cProfile and pstats :

import cProfile, pstats, StringIO

def profiler(func):
    def wrapper(*args, **kwargs):
        datafn = func.__name__ + ".profile"
        prof = cProfile.Profile()
        retval = prof.runcall(func, *args, **kwargs)
        s = StringIO.StringIO()
        ps = pstats.Stats(prof, stream=s).sort_stats('cumulative')
        ps.print_stats()
        print(s.getvalue())
        return retval
    return wrapper

Callback registration via a class instance:

class MyApp():
    def __init__(self):
        self.func_map = {}
    def register(self, name):
        def func_wrapper(func):
            self.func_map[name] = func
            return func
        return func_wrapper
    def call_method(self, name=None):
        func = self.func_map.get(name)
        if func is None:
            raise Exception("No function registered against - " + str(name))
        return func()

app = MyApp()

@app.register('/')
def main_page_func():
    return "This is the main page."

@app.register('/next_page')
def next_page_func():
    return "This is the next page."

print(app.call_method('/'))
print(app.call_method('/next_page'))

Logging decorator that prints function name, arguments, return value, execution time, and optionally the caller line number:

import time, inspect

def logger(loglevel):
    def log_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            ts = time.time()
            result = fn(*args, **kwargs)
            te = time.time()
            print("function   = " + fn.__name__)
            print("    arguments = {} {}".format(args, kwargs))
            print("    return    = {}".format(result))
            if loglevel == 'debug':
                line = inspect.currentframe().f_back.f_back.f_lineno
                print("    called_from_line : {}".format(line))
                print("    time      = %.6f sec" % (te - ts))
            return result
        return wrapper
    return log_decorator

MySQL query decorator (simplified) that injects query results into the wrapped function's keyword arguments:

import umysql
from functools import wraps

class Configuraion:
    def __init__(self, env):
        if env == "Prod":
            self.host = "coolshell.cn"
            self.port = 3306
            self.db = "coolshell"
            self.user = "coolshell"
            self.passwd = "fuckgfw"
        elif env == "Test":
            self.host = 'localhost'
            self.port = 3300
            self.user = 'coolshell'
            self.db = 'coolshell'
            self.passwd = 'fuckgfw'

def mysql(sql):
    _conf = Configuraion(env="Prod")
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            mysqlconn = umysql.Connection()
            mysqlconn.settimeout(5)
            mysqlconn.connect(_conf.host, _conf.port, _conf.user, _conf.passwd, _conf.db, True, 'utf8')
            try:
                rs = mysqlconn.query(sql, {})
            except umysql.Error as e:
                print(e)
                sys.exit(-1)
            data = handle_sql_result(rs)
            kwargs["data"] = data
            result = fn(*args, **kwargs)
            mysqlconn.close()
            return result
        return wrapper
    return decorator

@mysql(sql = "select * from coolshell")
def get_coolshell(data):
    ...

Simple asynchronous decorator using threading.Thread :

from threading import Thread
from functools import wraps

def async(func):
    @wraps(func)
    def async_func(*args, **kwargs):
        t = Thread(target=func, args=args, kwargs=kwargs)
        t.start()
        return t
    return async_func

@async
def print_somedata():
    print('starting print_somedata')
    time.sleep(2)
    print('print_somedata: 2 sec passed')
    # ...

The article concludes with references to the Python Decorator Library and various proposals for extending decorator capabilities.

pythoncode examplesProgramming FundamentalsdecoratorHigher-order function
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

login 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.