30.4 Registering Virtual Subclasses
In addition to explicit inheritance, the abc module provides a mechanism for registering classes as “virtual subclasses” of an abstract base class (ABC). This allows a class to be considered a subclass of the ABC for the purposes of issubclass() and isinstance() checks without having to inherit from it directly. This is a powerful tool for integrating third-party or existing classes into a type hierarchy that you control, promoting structural subtyping (duck typing) while maintaining the formal guarantees of nominal subtyping.
The register() Method
The primary method for creating a virtual subclass is the register() method available on every ABC. This method acts as a class decorator or can be called as a regular function. When a class is registered, it is added to the ABC’s _abc_registry, a weak set that keeps track of all its virtual subclasses.
from abc import ABC, abstractmethod
class MyABC(ABC):
@abstractmethod
def do_something(self):
pass
# A class that does NOT inherit from MyABC
class MyConcreteClass:
def do_something(self):
return "Done!"
# Registering it as a virtual subclass of MyABC
MyABC.register(MyConcreteClass)
# Now the type checks pass
print(issubclass(MyConcreteClass, MyABC)) # Output: True
print(isinstance(MyConcreteClass(), MyABC)) # Output: True
The critical distinction here is that registration only affects type checking. It does not enforce any interface contract. The MyConcreteClass in the example above would be instantiable even if it lacked the required do_something method. This makes register() a declaration of intent rather than an enforcement mechanism. It is the programmer’s responsibility to ensure the registered class actually fulfills the interface promised by the ABC.
Using register as a Decorator
The register method is most commonly and elegantly used as a class decorator. This syntax clearly indicates the relationship at the point of the class’s definition.
@MyABC.register
class MyOtherConcreteClass:
def do_something(self):
return "Done differently!"
def another_method(self):
# This is fine; virtual subclasses can have their own methods.
pass
print(issubclass(MyOtherConcreteClass, MyABC)) # Output: True
Why Use Virtual Subregistration?
The utility of this feature becomes apparent in several scenarios. Firstly, it allows you to adapt classes from libraries you don’t control. If you have an ABC Drawable and a third-party Square class that has a draw() method, you can register Square as a virtual subclass of Drawable to integrate it into your application’s type system without modifying the original library’s code.
Secondly, it can be used to group classes that share an interface but are part of unrelated inheritance hierarchies. For example, you might have a Serializable ABC. Both your custom User class and the built-in dict type could be made to implement a to_json() method. You can register both as virtual subclasses of Serializable to create a unified type category for serialization, even though dict cannot inherit from your ABC.
Caveats and Best Practices
The primary caveat has already been mentioned: registration does not enforce the interface. The ABC’s abstract methods have no effect on a virtual subclass. Because of this, it is considered a best practice to use virtual subregistration sparingly and only when you are confident the registered class fully implements the ABC’s interface. It is a tool for organizing existing code, not for defining new contracts.
Another important consideration is that a virtual subclass will not inherit any concrete methods implemented in the ABC. Since there is no actual inheritance, the virtual subclass must provide its own implementations for all methods, even non-abstract ones it might wish to use.
For a more robust alternative that does perform interface validation, consider defining the ABC to also be a metaclass and using __subclasshook__. This method allows an ABC to further customize the behavior of issubclass().
class StrictABC(ABC):
@abstractmethod
def required_method(self):
pass
@classmethod
def __subclasshook__(cls, subclass):
# Check if the subclass has the required method(s)
if cls is StrictABC:
if any("required_method" in B.__dict__ for B in subclass.__mro__):
return True
return NotImplemented
# This class will now pass issubclass() checks because it has the method.
class ValidClass:
def required_method(self):
pass
# This class will not.
class InvalidClass:
pass
print(issubclass(ValidClass, StrictABC)) # Output: True
print(issubclass(InvalidClass, StrictABC)) # Output: False
Often, the most powerful approach is to combine both techniques: use register() for external classes you are confident in and __subclasshook__ to allow other classes to be implicitly considered subclasses based on their structure, creating a flexible yet safe type system.