Adam Johnston

Writing a decorator in Python

Decorators are fairly uncommon in JS or TS although a few years ago they were kind of all the rage with the release of ES6 classes. I certainly remember Redux and MobX documenting a use of them for a time while NestJS makes use of them today. I think when it comes to React, their use fell off due to the transition away from class components to function components and the introduction of hooks. A limitation of decorator use in JS/TS is that they only work with classes. They can be applied to properties and methods but only as part of a class. Another thing to note is that they are still considered experimental, another turn off.

This isn’t the case in Python. You can of course use them with classes but you can also wrap a normal function and it might even be more “pythonic” to use a decorator instead of calling a function multiple times. I remember when I first saw something like myFunc(a)(b) in some now ancient jQuery documentation and was totally baffled. I had had no idea that was even a possibility.

Let’s write a simple decorator in Python and use it:

def provide_message(message: str) -> Callable:
    def decorator(func: Callable) -> Any:
        return func(message)
    return decorator


@provide_message("Hello")
def print_message(message: str) -> None:
    print(message)

When this code is run you’ll see Hello printed in the terminal. Something to be note is that we didn’t call print_message and yet we saw the message printed to the terminal and this is because the decorator function called it and passed along the message argument. Just a little thing to be aware of. The equivalent of this code without the use of a decorator would be the following:

print_greeting = provide_message("Hello")(print_message)

We have two problems with the above Python decorator, we probably don’t want our captured function print_message to be called when we apply a decorator to it. The other is that we can’t provide any more arguments to it. How do we solve this? With more functions of course! Lets do a little refactor:

def provide_message(message: str) -> Callable:
    def decorator(func: Callable) -> Callable:
        def capture(*args, **kwargs) -> Any:
            return func(message, *args, **kwargs)
        return capture
    return decorator


@provide_message("Hello")
def print_message(message: str, punctuation = "") -> None:
    print(message + punctuation)


print_message()
print_message("!")
print_message(punctuation="?")

Calling our little module we see:

Hello
Hello!
Hello?

Hooray! So we solved two problems using the capture function. One, we don’t immediately call our function when it is defined and only when it is specifically called and two we can pass arguments and keyword arguments.

Some learning resources