Mypy allows us to write class stubs which can be placed in the same directory as the actual class. This stub is very similar to an interface as known from other languages. Is it possible to have a client use the stub and the implementation strictly follow the stub?
Example I would like to work:
class IDependency:
def do_something(self) -> None: ...
def do_something_else(self) -> None: ...
class Service:
def __init__(self, dependency: IDependency):
dependency.do_something()
dependency.do_something_else() # this fails silently
class DependencyImplementation(IDependency):
def do_something(self) -> None:
print("doing something")
# Note there is no `do_something_else` here.
This works. However, if DependencyImplementation
doesn't implement the do_something
method, there is no error from Mypy and no error from Python itself. The call just doesn't do anything. Do I have to write raise NotImplementedException()
or annotate each method with @abc.abstractmethod
for this to work? Is there some special flags in Mypy or the Python interpreter?
Is this a use case for Mypy Protocols? It seems to be coming soon (maybe Python 4?)
This is indeed something you can do using either @abc.abstractmethod
or protocols. The former is akin to using Java's abstract classes; the latter will be kin to using Go's interfaces or Rust traits.
Here is an example that uses ABCs:
from abc import abstractmethod
class Parent:
@abstractmethod
def foo(self) -> None: ...
# Missing an implementation for 'foo'!
class Child(Parent): pass
print(Child()) # Error: Cannot instantiate abstract class 'Child' with abstract attribute 'foo'
A few things to note about this example:
foo
in that second subclass.abc
metaclass to Parent ( e.g. class Parent(metaclass=ABCMeta)
): mypy will understand what @abc.abstractmethod
means with or without it. Include the metaclass only if you want the Python interpreter to also enforce that you've correctly overridden anything marked as being abstract at runtime.You can also use protocols, though for now you'll need to first pip install typing_extensions
to use it. Here's an example:
from typing_extensions import Protocol
class CanFoo(Protocol):
def foo(self) -> None: ...
class Child: pass
def expects_fooable(x: CanFoo) -> None: ...
x = Child()
expects_fooable(x) # Error: Argument 1 to "expects_fooable" has incompatible type "Child"; expected "CanFoo"
A few notes:
Child
deliberately does not inherit from CanFoo
: there is no explicit link between a class and the protocol(s) it implements: protocols are very similar to Go-style interfaces and can be more ad-hoc. Contrast this to languages like Java, where you do need to include a "implements Blah" in the class definition.Child
: there's nothing inherently wrong with it. Rather, we get an exception when we try using it improperly.A few final notes:
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