Super dependency injection in Python
Tags: python, programming
Published: 2018-03-05 19:38:21

---
title: Super dependency injection in Python
published: true
description: super considered weird but good
tags: python, programming
---

_Originally posted on [my blog](https://incognitjoe.github.io/)._

This isn't a blog post about some great dependency injection framework I'm fond of. Rather, it's about using the `super()` method to change the behaviour of our classes in different contexts. If you're familiar with how super works in single inheritance languages, this might sound a bit mad - in my experience elsewhere, super calls out to the parent class, so if you have class B extending class A, and B calls super anywhere, then class A's method is invoked. However, this is not the case in Python - for classes constructed using multiple inheritance, Python's Method Resolution Order, better known as the MRO, changes dynamically to determine how super is invoked. In effect - if you use super in your class, then one of your children invokes super, the child's parents(of which your class is only one!) determines which method is invoked.

Confused? Let's step through an example and hopefully that'll clear things up.

So, simple enough situation, we're building some kind of service that makes HTTP calls to other services. We're going to use the [Requests](http://docs.python-requests.org/en/master/) library instead of writing all our own session handling and so on, but we do want to put some boilerplate stuff like custom headers, logging and error handling in one place, so we'll subclass `Session`:

```python
import requests


class CustomSession(requests.Session):
    def __init__(self):
        super().__init__()
        self.headers.update({'User-agent': 'CustomSession', 'Accept': 'application/json'})

    def get(self, url, **kwargs):
        print('Performing GET on {}'.format(url))
        resp = super().get(url=url, **kwargs)
        resp.raise_for_status()
        print('{} returned status: {}'.format(url, resp.status_code))
        return resp.json()
```

With that out of the way, we'll use our CustomSession as a base for our client:

```python
class MyAppClient(CustomSession):
    URL = 'https://myapp.corpdomain/'

    def get_stuff(self):
        resp = super().get(url='{}stuff'.format(self.URL))
        return resp
```

Hopefully your actual API wouldn't look so poorly, but that's not the point of this post. Anyway, with that done, `MyAppClient().get_stuff()` makes a GET request to `https://myapp.corpdomain/stuff`, throws an exception if the status is not 2xx/3xx, and returns the JSON response. Neat. However, we really do not want our tests making live HTTP calls constantly, so this might be a bit of a problem. Let's subclass CustomSession and make a TestSession that overrides our `get()` and just returns an empty dictionary for now:

```python
class TestSession(CustomSession):
    def get(self, url, **kwargs):
        print('TEST: app tried to GET {}'.format(url))
        return {}
```

Then let's do the complicated task of creating a test class that contains all the methods in MyAppClient but replaces the `get()` calls inherited from CustomSession with calls to TestSession:

```python
class TestMyAppClient(MyAppClient, TestSession):
    pass
```

Now when we run `TestMyAppClient().get_stuff()`, we see:
```bash
TEST: app tried to GET https://myapp.corpdomain/stuff
```

OK, so what's happening here? Basically the MRO _calls the next method, not the parent method_ in our classes. If we check the `help()` for our TestMyAppClient, we can see the resolution order:

```
 |  Method resolution order:
 |      TestMyAppClient
 |      MyAppClient
 |      TestSession
 |      CustomSession
 |      requests.sessions.Session
 |      requests.sessions.SessionRedirectMixin
 |      builtins.object
 |  
 |  Methods inherited from MyAppClient:
 |  
 |  get_stuff(self)
```

So, we have a `super().get()` call inherited from MyAppClient, _but_ TestSession is next in our resolution order, so that `get()` is invoked instead of the CustomSession's, despite MyAppClient having zero knowledge of TestSession.

Of course, this isn't just useful in testing - you could extend your CustomSession to fetch and parse `application/xml` responses instead of JSON, or have a `DebugSession` that adds an absurd amount of logging in place of our little print statements - once you're comfortable with determining how super is going to resolve, it opens up all kinds of possibilities.