Nathaniel Manista firstname.lastname@example.org added the comment:
I’d like to try to steer this conversation back toward what I think is the actionable question: “does the exemplification of this practice in the Errors and Exceptions portion of The Python Tutorial bring about a net benefit or a net cost to its intended audience?”.
What is the benefit? It is that when an author happens to be authoring a module *all user-defined Exception subclasses of which satisfy some more-specific-than-Exception type* the author is led to define an intermediate class between Exception and the directly-used user-defined Exception subclasses capturing that type in a usable code element.
What readers of the tutorial enjoy this benefit? Pretty much only those authors (who happen to be authoring a module *all user-defined Exception subclasses of which satisfy some more-specific-than-Exception type*) who are still learning about classes and inheritance. That’s a doubly slim population, isn’t it? Maybe also those who kind of know, but aren’t so sure and could use some reinforcement from seeing in the tutorial something that they independently did on their own in their own code. I wouldn’t think that authors who already know with confidence and from experience about classes and inheritance would benefit from the example in the tutorial, so “In my experience as a teacher, the possibility doesn't usually occur to people without it having been suggested” comes as a surprise to me. But… okay, them too - but again, only when they happen to be authoring a module *all user-defined Exception subclasses of which satisfy some more-specific-than-Exception type*.
What is the cost? It is that when an author happens to be authoring a module that does *not* have the property that all user-defined Exception subclasses satisfy some more-specific-than-Exception type, the common intermediate class is just bloat. It’s noise disrupting the signal. It undermines the API design advice “when in doubt, leave it out”. It unnecessarily widens the module’s API. It undermines the API design advice “classes are the most complex kind of code element in common use so they have the highest value proposition to satisfy to justify their existence”. It violates the Liskov Substitution Principle. Maybe most importantly, it obscures other relationships among the user-defined Exception subclasses in the module such as a superclass shared by some-but-not-all of the module’s user-defined Exception subclasses, if such other relationships exist.
What readers of the tutorial pay this cost? Those who are still learning the language. And those who are following pattern and convention - note that the tutorial contains only one example of user-defined Exception subclasses, and… an unfortunate fact of life of tutorials is that readers are invited to generalize from single examples. And, as I think we’ve seen in this conversation, those who just picked up the practice at one point and have kept it going.
The Exception subclass hierarchy of sqlite3 that was mentioned earlier in this conversation demonstrates exactly this bloat and misapplication - there’s Warning, which is a direct subclass of Exception, and there’s Error, which is also a direct subclass of Exception and has the erroneous specification “The base class of the other exceptions in this module”, and there’s DatabaseError, which is a subclass of Error, and then there are IntegrityError, ProgrammingError, OperationalError, and NotSupportedError, which inherit from DatabaseError. What’s the use of Error? There are no code elements in the module specified as raising Error. There’s example code in the module showing “except sqlite3.Error”, but why shouldn’t that be “except sqlite3.DatabaseError”?
It’s a red herring is that the practice appears widely applied in existing Python code - well of course; it’s been exemplified in the tutorial for seventeen years! :-P
One last thing to consider: look at the example specifically - InputError and TransitionError. There’s no elsewhere-in-the-module example code showing a function that has “Error” in its “Raises:” specification and could raise either an InputError or a TransitionError, and there’s no outside-of-the-module example code showing a user of the module calling a module function and saving duplicated lines of code because they want to respond to InputErrors and TransitionErrors in exactly the same way.
We should remove the “Base class for exceptions in this module” Error class from the tutorial’s example because it just isn’t beneficial enough, in enough applicable modules, to enough authors, and it’s more than costly enough, in enough modules to which it doesn’t apply, and to enough authors, even just as noise and API bloat. I don’t know that this could have been calculated from first principles seventeen years ago; I think perhaps it took the experience of having the guidance out there, so rarely merited and so widely implemented, to see it being a net drag.