Extending built-in Types

Sometimes you need an object that behaves exactly like a built-in type of Python, but you would like to also extend some behaviour of it. For this, the most common approach is to directly subclass the type.

For example, let’s imagine a fictional event system, on which we model our events as dictionaries, and we’d like to have some extra meta-data over the events. Something like the following code might be our first approach:

class Event(dict):

def __getitem__(self, key):

value = super().__getitem__(key)

return f"[{self.__class__.__name__}]: {value}"

If we try to use this code, we’ll find that it covers the basic cases as we might expect:

event = Event(user="user", event_type="login", date="2017-07-11")

print(event['user']) # [Event]: user

However, we want it to be completely compatible with a dictionary, meaning that has to actually be a dictionary. Here is where the tricky part and the surprises begin. Let’s try to iterate its keys and values to see what’s in there:

for key, value in event.items():

print(f"{key} = {value}")

Here we would expect the values with the transformation we defined (including the class of the event as prefix), but instead we get the underlying values of the dictionary, ignoring our implementation of __getitem__() . This code will print the following [3]:

user = user

event_type = login

date = 2017-07-11

What’s happening here is that the items() method doesn’t call our implementation of __getitem__() . Instead is using the underlying C implementation that doesn’t look for the rest of methods defined on the same object.

One more example: let’s say that now we want to log our events in a custom fashion, and for that we create a function like the following one, and we want to pass keyword argument from our dictionary.

def log_event_type(event_type, date, **kwargs):

print(f"{date} - {event_type}") log_event_type(**event) # 2017-07-11 - login

Again, we want our object to be a dictionary, so we’d expect that the ** would work normally, but again it’s not calling our method.

All of these problems are solved, if instead we subclass collections.UserDict . It’s available with the goal of creating dictionary-like objects in our code.

class Event(collections.UserDict):

def __getitem__(self, key):

value = super().__getitem__(key)

return f"[{self.__class__.__name__}]: {value}"

Just with this definition, all the aforementioned examples will work the way we expected to (the items, the keyword arguments, etc.). This is an implementation detail that you don’t need to know about, but the object is a wrapper around a dictionary (called data ), and when overriding the methods, this will be applied over the wrapped data. You don’t have to access the data attribute at all, the object itself will behave as a dictionary.