Turn Your Python Function into a Decorator with One Line of Code

A new way to write shorter, clearer, and more readable Python decorators that also act as a context manager

Turn Your Python Function into a Decorator with One Line of Code
Decorating made easier (image by Tobias Bjørkli on Pexels)

Do you want to write a decorator but can’t remember the syntax? Decorators have a pretty difficult syntax that involves a lot of boilerplate code. In this article we’ll demonstrate a simpler way of writing decorators. This new way will be much shorter, clearer, and more readable. Let’s code!


The default way to create a decorator

The code below is the default way to create a decorator. It times how long a decorated function runs. Check out this article for a deep dive in decorators.

def timer(name:str) -> Callable: 
    def decorator(func:Callable) -> Callable: 
        @wraps(func) 
        def decorator_implementation(*args, **kwargs): 
            try: 
                print(f"TIMER:   {name} start") 
                strt = time.perf_counter() 
                return func(*args, **kwargs) 
            finally: 
                print(f"TIMER:   {name} finished in {time.perf_counter() - strt}") 
        return decorator_implementation 
    return decorator

This way we can use our code like this:

@timer(name="test") 
def my_func(name:str, age:int) -> str: 
    return f"{name} is {age} years old" 
 
my_func(name="mike", age=34) 
# TIMER:   test start 
# mike is 34 years old 
# TIMER:   test finished in 5.299999999998532e-06

What’s the problem with this approach?

Personally I never remember how to write a decorator and always have to copy-paste code from old projects. I think this is because of the rather difficult-to-read syntax which involves 3 nested functions which make the decorator much harder to understand. Let’s find out how this simplify.

Python args, kwargs, and All Other Ways to Pass Arguments to Your Function
Expertly design your function parameters in 6 examples

An easier way to write a decorator

The implementation below does the exact same thing as the decorator in the previous part with only one function. It’s much more readable and we only need to add the @contextmanager decorator and convert the function to a generator by yielding.

@contextmanager 
def timer(name:str) -> Generator: 
    try: 
        print(f"TIMER:   {name} start") 
        strt = time.perf_counter() 
        yield 
    finally: 
        print(f"TIMER:   {name} finished in {time.perf_counter() - strt}")

All in all we can use the function in the exact same way:

@timer(name="AS DEC") 
def my_func(name:str, age:int) -> str: 
    return f"{name} is {age} years old" 
 
 
my_func(name="mike", age=34) 
# TIMER:   AS DEC start 
# mike is 34 years old 
# TIMER:   AS DEC finished in 5.399999999995686e-06

Functionality as a context manager

Personally I find the new function more readable and understandable. We need a few simple changes but they offer a lot of functionality in return. Decorating a function gets much easier and we can even use the decorator function (timer in the example above) as both a decorator and a context manager:

with timer(name='ctx_manager'): 
    print("In context-manager") 
 
# TIMER:   ctx_manager start 
# In context-manager 
# TIMER:   ctx_manager finished in 4.200000000002813e-06How is this possible?
Understanding Python Context-Managers for Absolute Beginners
Understand the WITH statement with lightsabers

Combining decorator and context manager

We can even use the same decorator function as both a decorator and a context-manager in the same block:

with timer(name='as ctx'): 
    fn_with_ctx_decorator(name="john", age=42) 
    print("In context-manager") 
 
# TIMER:   as ctx start 
# TIMER:   AS DEC start 
# john is 42 years old 
# TIMER:   AS DEC finished in 3.7000000000023126e-06 
# In context-manager 
# TIMER:   as ctx finished in 3.0000000000002247e-05
How to Store and Query 100 Million Items Using Just 77MB with Python Bloom Filters
Perform lightning-fast, memory efficient membership checks in Python with this need-to-know data structure

How does this work?

Under the hood, contextlib takes care of our decorator-function by using @contextmanager to wrap it in an object that act as both a decorator and a context-manager. The way this works is pretty technical, very interesting and requires an article of its own. Follow me to stay tuned!

For the scope of this article it’s enough to know that contextlib uses the @contextmanager decorator to convert out decorator-function into an object that can be used as both a decorator and contextmanager.


Downsides

Although the new decorator offers us a lot of functionalities , there are some downsides. The most prominent one is that within our decorator-function, we have no access to the function that we’re actually decorating. We also have no access to its args and kwargs. This prevents us from modifying these variables but in my opinion this is something you should rarely do.

The second downside is that the decorator-function must be a generator-function. This means that it must yield somewhere in its body. This may force you to rewrite a bit of your code.

Python: __init__ is NOT a constructor: a deep dive in Python object creation
Tinkering with Python’s constructor to create fast, memory-efficient classes

Conclusion

With @contextmanager writing a decorator is easy and readable. It takes care of a lot of boilerplate and even acts as a contextmanager. The downside is that for all this automation and “magic under the hood”, you lose some control in the form of access to your function and arguments.

The way contextlib works under the hood is pretty complex and deserves an article on its own, so follow me if you’re interested!

I hope this article was as clear as I hope it to be but if this is not the case please let me know what I can do to clarify further. In the meantime, check out my other articles on all kinds of programming-related topics.

Happy coding!

— Mike

P.S: like what I’m doing? Follow me: