I've several times run into a problem with adding type annotations to existing code, where an abstract base class has a method and then subclasses implement the method and add some optional keyword arguments. Let's say the code looks something like this:
class Base:
def foo(self, x: int, **kwargs) -> None: ...
class A(Base):
def foo(self, x: int, *, frob: bool = False) -> None: ...
class B(Base):
def foo(self, x: int, *, extra: str = '') -> None: ...
Mypy will reject this because the subclass implementations are less general than the base class (because they don't allow for arbitrary keyword arguments).
I could remove the kwargs
from the base class, but that then causes problems with functions that accept any instance of Base
and passes arbitrary keyword arguments through e.g.:
def call_base(x: Base, **kwargs) -> None:
x.foo(3, **kwargs)
Mypy then complains because Base.foo doesn't accept arbitrary keyword arguments.
Another option is to have the derived-class implementations take an (unused) kwargs
, but that also has disadvantages:
- It's less precise, so mypy will be less useful when the instance is known (statically) to be of one of the derived classes.
- If the user passes an incorrect keyword argument, it will now be silently ignored (unless extra code is added to check for this) rather than raising an exception.
Is there some way I can have my cake and eat it i.e. tell mypy not to check additional arguments when invoked on the base class, but to do so on the derived class? One might think this violates the typing model (derived-class methods cannot narrow the accepted parameters), but it's already possible to do for an individual parameter (declare it as Any
in the base class but a specific type in the derived class).