Well, this turns out to be trickier than it seens at first -
because basically you want to have class inheritance relationship, but do not use the normal attribute lookup paths on class inheritance -
Otherwise, HTTPError, being a subclass of BaseError, for example, would always have all the attributs present in BaseError itself - Therefore,
the chain BaseError.HTTPError.HTTPError.HTTPError.HTTPError... would always be valid.
Fortunately, Python does offer a mechanism to register classes as subclasses of other, without "physical" inheritance - that is, it is reported as subclass, but does not have the parent class in its bases or __mro__ - and therefore, attribute lookup on the derived class (adopted?) does not search attributes in the "foster" parent.
This mechanism is provided through the "abstract base classes" or "abc"s, through its ABCMeta Metaclass, and "register" method.
And now, due to the fact you also probably want to declare
your class hierarchy with the normal inheritance syntax - that is,
being able to write class HTTPError(BaseError): to indicate the new
class derives from BaseError - you get the actual "physical" inheritance.
So, we can inherit from ABCMeta class (instead of type) and write
the __new__ method so that the physical inheritance is excluded -
and we use the setattr for containment you intended with your code as well, and also, we trigger the needed call to parentclass.register directly on the metaclass.
(Note that as we are now changing the base classes, we need to fiddle
in the __new__ method of the metaclass, not on __init__:
from abc import ABCMeta
class MetaError(ABCMeta):
def __new__(metacls, name, bases, attrs):
new_bases = []
base_iter = list(reversed(bases))
seen = []
register_this = None
while base_iter:
base = base_iter.pop(0)
if base in seen:
continue
seen.append(base)
if isinstance(base, MetaError):
register_this = base
base_iter = list(reversed(base.__mro__)) + base_iter
else:
new_bases.insert(0, base)
cls = super(MetaError, metacls).__new__(metacls, name, tuple(new_bases), attrs)
if register_this:
setattr(register_this, name, cls)
register_this.register(cls)
return cls
And for a quick test:
class BaseError(Exception):
__metaclass__ = MetaError
class HTTPError(BaseError):
pass
class HTTPBadRequest(HTTPError):
pass
In the interactive mode, check if it works as you intend:
In [38]: BaseError.HTTPError
Out[38]: __main__.HTTPError
In [39]: BaseError.HTTPError.HTTPError
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-39-5d5d03751646> in <module>()
----> 1 BaseError.HTTPError.HTTPError
AttributeError: type object 'HTTPError' has no attribute 'HTTPError'
In [40]: HTTPError.__mro__
Out[40]: (__main__.HTTPError, Exception, BaseException, object)
In [41]: issubclass(HTTPError, BaseError)
Out[41]: True
In [42]: issubclass(HTTPBadRequest, BaseError)
Out[42]: True
In [43]: BaseError.HTTPError.HTTPBadRequest
Out[43]: __main__.HTTPBadRequest
In [44]: BaseError.HTTPBadRequest
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-44-b40d65ca66c6> in <module>()
----> 1 BaseError.HTTPBadRequest
AttributeError: type object 'BaseError' has no attribute 'HTTPBadRequest'
And then, most important of all, testing if the Exception hierarchy actually works in this way:
In [45]: try:
....: raise HTTPError
....: except BaseError:
....: print("it works")
....: except HTTPError:
....: print("not so much")
....:
it works
A few notes: no need to inherit from both Exception and object explicitly - Exception itself already inherits from object. And, most important: whatever project you are working on, do whatever is possible to move it to Python 3.x instead of Python 2. Python 2 is with the days counted, and there are many, many new features in Python 3 you are excluding yourself of using. (The code in this answer is Python 2/3 compatible, but for the __metaclass__ usage declaration of course).