Using Python's multiple inheritance (MI) for interfaces, abstract base classes, mixins, or similar techniques is perfectly fine. In most cases, the MRO produces intuitive results.
However, object initialization under multiple inheritance is really tricky. In Python you cannot combine multiple classes per MI unless all participating classes have been designed for MI. The issue is that the
__init__() method cannot know from which class it will be called and what the signature of the
super().__init__() method will be. Effectively, this means that MI constructors:
- must call the
- must only take arguments by name, not by position
- must forward any
**kwargs to the
- must not warn on unexpected arguments
Where possible, the better alternative is to avoid
__init__() methods for interface-like classes, and instead express requirements through abstract methods. For example, instead of a
BananaContainer class, we might write this interface/ABC:
import abc # abstract base class
def bananas(self) -> list:
If a class wants to be a BananaContainer, it would have to implement that property.
In general, it is perfectly alright if you have a class that inherits from multiple interfaces or mixins. Aside from name collisions, the above
__init__() problems, and general API bloat of the class, no noteworthy issues arise.
The second part of your question proposes a capability-based approach instead of using inheritance. Using composition instead of inheritance is often a very very good idea. For example, you eliminate the initialization problems by design. It also tends to lead to more explicit APIs that are easier to navigate and avoid name clashes. There should be some method that either returns an object representing a capability, or
None if the capability isn't supported.
But these capabilities can be implemented in different ways: either by using normal composition, or by storing the capabilities in your own data structures.
Unless you have special needs for the object model, stick to the language. Store methods in normal object fields, provide normal methods to access them. This leads to a more comfortable API, and is more likely to support auto-completer and type-checkers.
If you need to modify the available capabilities of an object at run-time, and need to introduce new kinds of capabilities at run-time, then using a dictionary may be appropriate. But at this point you are inventing your own object system. This may be a good idea e.g. in games that have complex capability systems where new capabilities shall be defined in configuration files.
Most software does not have these requirements, and does not benefit from that kind of flexibility.
Additionally, Python's built-in object system is flexible enough that you could create new types and new methods without having to create a new object system. Builtins like
hasattr(), and the
type() constructor come in handy here.
I would likely express an object that can have both AppleContainer and BananaContainer capabilities like this:
def __init__(self, x, y, z):
# somehow determine the appropriate capabilities and initialize them
self._banana_container = BananaContainer(y) if x else None
self._apple_container = AppleContainer(y)
def as_banana_container(self) -> Optional[BananaContainer]:
def as_apple_container(self) -> Optional[AppleContainer]:
o = HasCapabilities(...)
bc = o.as_banana_container
if bc is not None:
Or with Python 3.8 assignment expressions:
if (bc := o.as_banana_container) is not None:
If you want to have some custom mechanisms for reflection over capabilities, you can implement that on top of this solution, with some amount of boilerplate. If we want to be MI-safe, we might declare the following base class that all capability-having classes need to inherit:
# a base implementations so that actual implementations
# can safely call super()._get_capabilities()
"""deduplicated set of capabilities that this object supports."""
def get_capability(self, captype):
"""find a capability by its type. Returns None if not supported."""
which in the above case would have been implemented as:
caps = [ # go through properties in case they have been overridden
yield from (cap for cap in caps if cap is not None)
yield from super()._list_capabilities()
def get_capability(self, captype):
if captype == BananaContainer:
if captype == AppleContainer: