Some quotes to provide context.

"I read several times that ideally I should avoid inheritance with ABCs and if I do, then from a standard class. That is, I should avoid creating a typical base and subclass construct that is used in a strategy pattern, for example."

"If I look at a principle like SOLID, where we have the Liskov Substitution Principle, the interface separation principle, and the dependency inversion principle, they all rely on a base class, which of course is created by inheriting from ABC."

"Python, on the other hand, does not have the strong need to be object-oriented, but most of the principles assume that we are working with classes."

"Working with Liskov Substitution can make it difficult to navigate through an IDE."

BLUF

ABC's are only required as a way to do earlier validation that a class is complete.

Otherwise, they're not required.

They can be helpful when planning dependency injection. The SOLID Dependency Inversion Principle advises us to depend on abstractions, not concrete, specialized subclasses.

In Python, we don't need an abstraction at the base of a class hierarchy.

Purely OO Design?

First, I want to talk a little bit about OO design and "purely" OO languages.

Unlike Java or C++, Python is purely object-oriented. Not that it actually matters.

A functional style of Python depends on functions which are -- essentially -- callable objects. They have one method, __call__(). They have attributes. Because they descend from the object class, you can add attributes to a function. They're instances of a type, function.

Java and C++ have "primitive" types which aren't objects; they're not "purely" OO. Python doesn't have this quirk. Python is more purely object-oriented than Java. Which suggests any OO purity test doesn't really matter.

OO Design Principles and Functional Design Principles can all be used with Python. Indeed, some of the old COBOL design patterns can be used, too. (Not all, of course. COBOL had GOTO's, an ALTER statement, and a very weird PERFORM THRU that make it right weird to map to Python.)

OO Purity? Doesn't matter.

Let's move on to look at Python's ABC's. After that, we'll look at the SOLID principles in general.

To ABC or not to ABC?

C++ and Java (and many other languages) are built around separately-built binaries that are linked together. Some linkage can be done at compile time, some can be done at run time by the OS loader. Since they're separately-built binaries, everyone must agree on the binary interface. Change cannot be tolerated.

I'll emphasize that.

Emphasis

C++ and Java emphasize a style of design where interface changes cannot be tolerated.

Pragmatically, we now have computers that are so fast that recompiling a very large Java or C++ app is no longer a nightmare of waiting for hours. When these languages were designed, a development team might do one nightly build of everything. During the day, you were limited to compiling the classes you were working on. Nothing more. Getting the interfaces to be stable was an important risk reduction technique.

Abstract Base Classes were a way to minimize recompilation.

Important

Abstract vs. Concrete

There are abstract base classes and concrete base classes. The abc module introduces a whole bunch of stuff to support abstraction. Ordinary class Special(Die): inheritance from a concrete base class involves no abstraction, and no abc.

Python eschews strict class hierarhcies, and replaces this with "Duck Typing". All an object requires is to have the method defined.

See The eval() Conundrum and Python-as-DSL. Way at the end is this snippet of code.

class Die:
    def __init__(self, faces: int) -> None:
        ...
    def __rmul__(self, n: int) -> "Die":
        ...
    def __add__(self, adj: int) -> "Die":
        ...
    def roll() -> int:
        ...
    @property
    def min(self) -> int:
        ...
    @property
    def max(self) -> int:
        ...

D4 = Die(4)
D6 = Die(6)
D8 = Die(8)
etc.

This means an object like D4 can be used with the * and + operators. In very limited ways.

The expression 6 * D4 is legal, where D4 * 6 is not. The way Duck Type works, there's a search for a method to implement *.

Consider 6 * D4.

  1. Does 6 implement __mul__()? It does. However, when int.__mul__() is evalated with a Die object, the result is NotImplemented.
  2. Does Die implement __rmul__()? It does. When Die.__rmul__() is evaluated with an int object, the result is a new Die object.

(There's actually a first step, omitted for brevity. See the sidebar.)

Here's the bottom line.

The Duck Typing two-step search for matching method names doesn't respect the class hierarchy.

A Protocol formalizes the Duck-Typing Two-Step in a way that tools can be sure the whole

Summary:

  1. When using only Python, most developers don't need to care about separately-compiled binaries. (When writing Rust or C extensions, of course, separately-compiled binaries are a big deal.)
  2. Duck typing eliminates a requirement for ABCs.

Conclusion

Is an Abstract Base Class still helpful?

Yes.

When?

When you need it.

When is it necessary?

The use case for an ABC in Python is to push the Duck-Typing Two-Step so it happens earlier.

  1. ABC's permit type hint checking to be sure the code is likely to work.
  2. At run time, ABC's prevent instantiating an incomplete object. Code may crash earlier.
    Most important: the exception is much more clear when a method is missing.

ABC's promote early detection of design problems.

When is it superflous?

When your base class is concrete, don't waste time on an ABC. Just use a concrete base class and extend it as needed.

That's enough on ABC's for now. Let's move on to the SOLID principles.