A few months ago, @cfbolz posted this tweet

As a reply, I had tweeted

This is one of the few cases where python gives a confusing error message. Let us take a deeper look into this, and in the process get a chance to understand some CPython internals.

What is a layout?

multiple bases have instance lay-out conflict

In order to understand this message, we need to know what is meant by layout.

Layout refers to how the object is represented in memory. Suppose we have a class like this

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

The memory is laid out like this:

The object consists of various fields that python needs to track. How all those pieces is arranged in memory is called the layout of the object. In python's case, all the information is arranged sequencially in "slots". Each slot stores one piece of data. One of these slots is used to store the __dict__ field which references a dictionary object. Every instance variable is stored in the dictionary as a triplet containing the attribute hash, the attribute name and the attribute value.

Note that python will always allocate a dictionary of a minimum size, so even if you have just one attribute, python will allocate space for multiple entries.

Why cannot python store all the instance data in the slots itself? This is because the object layout is fixed. Once the memory is allocated, the layout is not changed until the object is deleted. This is why python creates a dictionary to store the instance fields. It allows us to dynamically add and remove attributes without having to change the layout.

⚠️
Note that the layout is an internal detail of CPython and can change between versions. In fact Python 3.11, releasing soon, will be changing the layout again. It is the concept that is important here, not the exact layout

This design contributes to the dynamic nature of python, but it comes with two disadvantages:

  1. It takes more memory to create a dictionary and store the fields in that, compared to just storing the fields directly in a slot. Especially creating a whole dictionary when you want to keep just two or three fields is quite wasteful
  2. It takes longer to lookup attributes, as you need to hash the dictionary keys and lookup the attributes in the dictonary, compared to just looking up the value directly from a slot

Slots

Normally, these would not be a big deal as the extra work is just a little and the dynamic benefits make the trade-offs worthwhile. But when you have huge number of objects of a class being created and used then those little extras can add up to something significant.

To solve this, Python introduced __slots__. This feature exposes the slot structure used internally by python. It allows the developer to define the attributes of the class beforehand and by doing that, python will use that information to create a more efficient layout. Take this code for instance

class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

The layout for this class will be as follows

As you can see, this layout does not have a  __dict__ field anymore – so we can no longer add or remove fields dynamically.The upside is that the field values can be retrieved directly from the layout without having to go through the dictionary. This makes it more efficient from a time and memory perspective.

Well, you could add the __dict__ field as one of the fields in the __slots__, and you will get dynamic field creation again. But that would mostly defeat the point of using __slots__

Note that slots are referenced using position, so the x attribute is configured to use slot 0 and y attribute is configured for slot 1. (To be more precise, slots are configured by the offset from the start of the object memory)

Internally slots are implemented using descriptors. We will discuss how exactly they work in a future article. 

Slots and Inheritance

When inheritance is involved, the __slots__ on the parent class are automatically inherited. The child class can create additional slots if needed as in this example

class Point3D(Point):
    __slots__ = ('z',) # only mention the additional slots
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

The Point3D class would be laid out like this

Note how it includes the slots from the base class in it, and then adds the additional slots to the layout. The slot configuration is also inherited, so  x is slot 0 and y is slot 1 is inherited, and then a new configuration is added to z for slot 2.

Slots and Multiple Inheritance

Okay, this is where things start to get messy (Isn't that always the case with multiple inheritance?). The rule with slots and multiple inheritance is that only one of the parents can have slots. Why this rule?

Let us take an example. Suppose we have these two classes

class PointX:
    __slots__ = ('x',)
    
class PointY:
    __slots__ = ('y',)
    

In PointX the field x is assigned to slot 0. Similarly in PointY the field y is assigned to slot 0. No problems so far.  

Now we try to inherit from both of them

class Point(PointX, PointY):
    pass

Remember the class inherits the slots as well as the slot assignments. It inherits the configuration of attribute x for slot 0 and attribute y for.... slot 0. Ooops. And this is the problem. Python is unable to create a layout because it cannot merge the layouts of the base classes. And we get the infamous error multiple bases have instance lay-out conflict

We are finally ready to understand what this error means. You get the error when you inherit from more than one base class that uses slots, because python is unable to layout the class due to conflicting layouts in the parent classes.

What changed in Python 3.10?

That brings us back to the original tweet. The code below works in Python 3.9 but it gives the layout conflict error in Python 3.10

class A(AttributeError, OSError): pass

What changed? Well it turns out that AttributeError started using slots in 3.10 (OSError has used slots for a while), and when you try to inherit from both, you now get an error. Here is @cfbolz again

Fortunately, this example is specifically created to show the change that has happened in Python 3.10. In practise, you probably have no reason to multiple inherit from two exception classes, and chances you will run into this problem when upgrading to 3.10 are extremely slim.

Did you like this article?

If you liked this article, consider subscribing to this site. Subscribing is free.

Why subscribe? Here are three reasons:

  1. You will get every new article as an email in your inbox, so you never miss an article
  2. You will be able to comment on all the posts, ask questions, etc
  3. Once in a while, I will be posting conference talk slides, longer form articles (such as this one), and other content as subscriber-only

Tagged in: