Dependency Injection (like pytest fixtures) with a Python decorator

If you’ve used Pytest, you’ll notice a neat little pattern. If you want a particular fixture, you just ask for it, seriously, check out the docs:

@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

test_fruit_salad gets called when you run tests, and it will automatically pass the result of fruit bowl to test_fruit_salad. This pretty much blew my mind when I first saw it.

The following decorator helps you achieve something similar with your own functions. It lets you do something like this.

@opt_in_args
def request_a(foo: str, *, a: int):
    print(f"Foo is {foo}, a is {a}")

@opt_in_args
def request_b(*, b: str):
    print(f"b is: {b}")

@opt_in_args
def request_both(*, b: str, a: int):
    print(f"b is: {b}, a is: {a}")

request_a("Blarg")
request_b()
request_both()

Note that the * args just mean “everything after this is a keyword argument only”, it helps guard against accidentally passing in bad kwargs and you should always use it, but the above program will work the same without it. The output of the program looks like this:

Foo is Blarg, a is 123
b is: foobar
b is: foobar, a is: 123

First let’s look at the implementation of the decorator:

import inspect

def opt_in_args(func):
    def wrapper(*args, **kwargs):
        signature = inspect.signature(func)
        opted_in = {}
        optional_kwargs = {
            "a": 123,
            "b": "foobar",
        }
        for kwarg, value in optional_kwargs.items():
            if kwarg in signature.parameters:
                assert signature.parameters[kwarg].annotation == type(value)
                opted_in[kwarg] = value
        return func(*args, **kwargs, **opted_in)
    return wrapper

Let’s look at a few specific lines to illuminate what it’s doing:

def opt_in_args(func):
    def wrapper(*args, **kwargs):

This is standard decorator magic. The outer function has the function in-scope, the wrapper is (because we return it from the decorator) what actually gets called in place of the wrapped function, so we capture *args and *kwargs so we can pass them along.

signature = inspect.signature(func)

Note that we imported inspect at the top of the module. Inspect lets you introspect on python functions, and in this case we use it to grab the signature. signature.paremeters will now have a key for each parameter the function accepts; we can now write any code we like based on the params.

        optional_kwargs = {
            "a": 123,
            "b": "foobar",
        }
        for kwarg, value in optional_kwargs.items():
            if kwarg in signature.parameters:

This approach is totally optional. There are any number of ways you can enumerate the optional kwargs. You can make another decorator and supply functions which are only called if they’re requested (I think pytest fixtures must be doing something like this) or you can have a bunch of ifs to decide what to instantiate and put in.

                assert signature.parameters[kwarg].annotation == type(value)

This I just put in there to show one way you could enforce typing on the keyword arguments. This is of course also optional; you could write your decorator to only care about the name by removing this line.

                opted_in[kwarg] = value
        return func(*args, **kwargs, **opted_in)

We create a dictionary called opted_in and populate it with the additional kwargs we want to pass. The final line of the wrapper function calls the wrapped function with the args and kwargs the caller sent, unmodified, and finally with the opted_in args the function asked for in its signature.

For such a slick effect, this is a surprisingly easy decorator; decorator magic often is.

Python list comprehensions

If, like me, you learned python and programming at the same time, you may have missed out on advanced features that, while awesome in python, won’t carry over to other languages you’ll “graduate” to using. One of these features is a list comprehension. It lets you in a compact (and readable) way write loops that take a list and return a list, and does not require lambda syntax. They let you write this:

list_squared = []
for x in [1, 2, 3, 4, 5]:
    list_squared.append(x ** 2)

like this:

[x ** 2 for x in [1, 2, 3, 4, 5]]

This really shines when you’re transforming and extracting data – you can also stick an ‘if’ at the end, turning this:

odds_squared = []
for x in [1, 2, 3, 4, 5]:
    if x % 2:
        odds_squared.append(x ** 2)

into this

[x ** 2 for x in [1, 2, 3, 4, 5] if x % 2]

It might not be a huge improvement in amount of code (or LOC, since you’ll want to break complex list comprehensions across multiple lines) but it saves you from potential mistakes, typing ‘append’, and (importantly) having to declare and use another variable name. I consider that a win.

If you found this post exciting, generator expressions will blow your mind.