Are there any good conventions for what exception should be raised by a metaclass or __init_subclass__ that enforces certain invariants on subclasses?
For this answer I would accept precedent from Python built-in behavior, PEPs, the standard library, or well-established third-party libraries, or compelling reasoning for why it should be done a certain way.
The precedent I know of within Python itself is to just always raise TypeError:
TypeError:
>>> class MorePowerfulTuple(tuple): pass
... __slots__ = ('power',)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: nonempty __slots__ not supported for subtype of 'tuple'
bool, it raises a TypeError:
>>> class BetterBool(bool): pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'bool' is not an acceptable base type
>>> class StrongerFasterMethodType(types.MethodType):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'bound_method' is not an acceptable base type
The most significant precedent-setter here is the first one. The latter two are arguably type-specific subclassing errors, because they're specifically errors due to trying to use a wrong type as input the subclassing operation (so if you had just those examples, that is still consistent with other exception types being more appropriate if the subclassing is erroneous for some other reason). But the first one is raising a TypeError even though the problem is not necessarily type-specific: you could replace __slots__ = ('power',) with __slots__ = () and it would work. So we've got at least one precent in the reference implementation of the language itself of a subclassing error being reported as a TypeError, even though in many other situations, using an empty iterable rather than an iterable with one or more elements would be more appropriately a ValueError.
There does not seem to be any class in the built-in exception hierarchy designed for this purpose.
Also, that documentation rules out trying to use multiple inheritance for the subclassing errors like these (which could've been tempting in some situations - for example, the implementor of a metaclass or __init_subclass__ might imagine a factory function dynamically making subclasses on the fly in the middle of program execution based on some parameters, and think it would be helpful to raise a more precise error like ValueError - so a developer might decide it's a good idea to multiply subclass TypeError and ValueError and maybe their own subclassing error and ValueError, instead of just raising a plain TypeError as above built-in class precent does), because that documentation explicitly recommends not subclassing from multiple exception types, due to internal implementation details being free to implement different exceptions in a way that might make it impossible to use them both as base classes:
Some have custom memory layouts which makes it impossible to create a subclass that inherits from multiple exception types. The memory layout of a type is an implementation detail and might change between Python versions, leading to new conflicts in the future. Therefore, it’s recommended to avoid subclassing multiple exception types altogether.
I don't know of any PEPs, standard libraries, or third-party libraries establishing a specific practice.
(However, there are definitely libraries, such as namedtuple, dataclasses, attrs, and probably even ones like SQLAlchemy and Pydantic, which programmatically generate classes based on user parameters, that might be worth looking at for their own examples.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With