Where would you use `typing.Protocol` where you wouldn't use an abstract base class (or a bunch of them as mixins)? My understanding is that the latter is more "formal", whereas `Protocol` is better suited for gradual typing (i.e., moving a more "free-form" codebase into something MyPy won't complain about).
The only time I've ever used `Protocol` is to define a type that makes it explicit that I need an object to have a `__str__` implementation:
@runtime_checkable
class Stringable(Protocol):
""" Protocol type that supports the __str__ method """
@abstractmethod
def __str__(self) -> str:
""" Return the string representation of an object """
I've since learned that this is redundant, because all Python objects have an implicit `__str__` if one isn't specified (IIRC). When I didn't know this (when I implemented the above), I didn't use an ABC because obviously I can't guarantee all objects (e.g., those outside of my control) are subclasses of said ABC. The cases where this is true are vanishingly small, especially when you go big on dependency inversion.
This distinction is the same as the distinction between structural and nominal typing, described at the end of the article.
Abstract base classes require everything to extend from a base-level object, and also inherit it's default implementations. This is a nominal (name-based) mechanism.
Protocol (structural-based) subtyping instead only declares what methods something has to have, without requiring it tie itself to either to a concrete parent class and its behaviours.
Edit: since you partially address this, you ask why you'd use structural typing rather than an abstract base class. I'll turn it around and ask why you would use an abstract base class, which requires modifying the children accordingly, when you could instead use a structural type. The usual preference comes down to whether you want to be explicit about implementing it or not, and whether you want to pull in default behaviour.
> The usual preference comes down to whether you want to be explicit about implementing it or not, and whether you want to pull in default behaviour.
Thanks :) I definitely prefer things to be explicit; regardless of it being a "Zen of Python" mantra. That said, I do see the value of `Protocol` for non-annotated/legacy code, in that it gives you a convenient mechanism for this. I would be worried about it becoming a crutch, if overused, but definitely better than no annotations at all!
(I don't understand your point regarding an ABC's default implementation. Isn't the point of ABCs that they're not base classes, but interfaces that define the methods required without any implementation?)
> Abstract base classes require everything to extend from a base-level object, and also inherit it's default implementations. This is a nominal (name-based) mechanism.
The very definition of an abstract base class precludes the existence of (meaningful) implementations. If the base class has implementations for the methods expected to be overridden (beyond no-ops or throwing some exception around the lack of an implementation), then it is definitionally not abstract.
That is: a protocol and an abstract class achieve the same thing: they declare the existence of methods, and defer implementation of those methods to implementations / child classes (respectively).
Interfaces are great, but if you have multiple inheritance as your hammer (as you do in Python), then it's perfectly reasonable for abstract class inheritance to be your nail.
(meaningful) isn't as much a get-of-out-jail clause here as you suggest, if I understand correctly. Requiring implementations at all, even if they are broken, moves the time when you find out about a missing definition to runtime, rather than being statically determined at typecheck time.
If the not-overriden method isn't commonly called, it's possible for the non-meaningful / raise an exception version to make it to production. Not so if the error happens at typecheck time.
Right, but that's a problem more fundamental to Python and applicable far beyond the "abstract" classes hacked onto it. That is: if the methods were missing entirely, you'd have the same problem, since Python (without the help of external tools) doesn't make any attempt to validate these things at (bytecode) compile time.
That is true. However, other linters (e.g., pylint) will check a class satisfies its interface. IIRC, the runtime error for non-implemented abstract methods happens at instantiation time, rather than when said missing method call is attempted.
> The very definition of an abstract base class precludes the existence of (meaningful) implementations.
No, it doesn't. It precludes meaningful implementation of the complete interface, of course, but it doesn't preclude meaningful implementation for some methods, which depend on the other methods do which meaningful implementations are not provided.
Right, but those methods which are implemented are not in the set of methods intended for descendant classes to override. If they are in that set, at all, then the base class is by definition not abstract.
The fundamental issue is that Python doesn't really have a way to enforce this, so there's ultimately no such thing as an abstract class in Python - there are only classes that act like they're abstract (by replacing all implementations of meant-to-be-overridden methods with no-ops or thrown exceptions).
> The fundamental issue is that Python doesn't really have a way to enforce this
When (as to make this even worth discussing, one must) one includes the typecheckers (which are technically external) in “Python”, that's not true; both mypy and pyright enforce that abstract classes (either those explicitly declared as abcs, or derived classes with any non-overridden declared-as-abstract methods) cannot be instantiated.
> Abstract base classes require everything to extend from a base-level object, and also inherit it's default implementations
The general OOP concept of an “abstract base class” might, the particular Python implementation does not, because, as well as classic inheritance, it supports the concept of “virtual subclasses” that are registered with, but do not actually inherit from (and thus do not use the implementation of), an abc.
It shares with structural subtyping that it only defines a mandated interface, but with nominal subtyping that it requires an explicit statement of intent to conform to the interface before an object is considered to conform.
The only time I've ever used `Protocol` is to define a type that makes it explicit that I need an object to have a `__str__` implementation:
I've since learned that this is redundant, because all Python objects have an implicit `__str__` if one isn't specified (IIRC). When I didn't know this (when I implemented the above), I didn't use an ABC because obviously I can't guarantee all objects (e.g., those outside of my control) are subclasses of said ABC. The cases where this is true are vanishingly small, especially when you go big on dependency inversion.