2026, Jan 07 19:00

Overriding Abstract Methods in Python: Keep Signatures Compatible to Honor LSP and Substitutability

Why narrowing signatures when overriding abstract methods in Python breaks LSP and substitutability, how *args/**kwargs impact mypy, keyword-only overrides.

Overriding abstract methods with a more specialized signature often “works” in Python, but the fact that it runs doesn’t automatically make it a sound design. The crux is whether the derived method still honors the contract that the base type promises. If the override narrows what callers are allowed to pass, you risk breaking substitutability and surprising anyone who uses your class through the abstract interface.

Problem demonstration

Consider a base class that declares a highly permissive method and a subclass that tightens the signature. The following code runs, yet it encodes a subtle design issue.

from abc import ABC, abstractmethod
class Root(ABC):
    @abstractmethod
    def act(self, *args, **kwargs):
        raise NotImplementedError()
class Impl(Root):
    def act(self, x, y, *args, **kwargs):
        print(f"Impl.act(x={x}, y={y}, rest={args}, opts={kwargs})")
obj = Impl()
obj.act(1, 2, 3, "bar", flag="baz")
# output:
# Impl.act(x=1, y=2, rest=(3, 'bar'), opts={'flag': 'baz'})

What’s the actual issue?

The abstract method accepts any positional and keyword arguments, which means the base contract suggests “you can call this with anything.” The override in the subclass requires at least two positional arguments. That is a narrower requirement than the abstract interface signals. Using the subclass where the base is expected would then allow calls that the subclass can’t handle, violating the idea that everything accepted by the abstract parent must also be accepted by the derived implementation.

In type-theory terms, method arguments are contravariant: acceptable inputs for an override should get broader, not narrower. If you require more specific argument shapes in the derived class, you shrink the set of valid calls, breaking the promise implied by the abstract method. This is essentially a Liskov Substitution Principle concern.

There’s also a practical pitfall with static analysis. A stricter override like the example above can slip past mypy in some situations, because mypy doesn’t check unannotated functions by default, and it has a special case that lets a (self, *args, **kwargs) method be overridden by any signature at all, even when it does check. The code may type-check in practice, yet still not be type-correct from a substitutability standpoint.

What to do instead

The safe pattern is to keep the abstract method’s signature as minimal and stable as the contract demands, and then augment behavior in the subclass without breaking that contract. If the base says “no arguments,” the override may still accept no arguments. The subclass can expose opt-in tweaks as keyword-only parameters so that everything the parent accepts is also accepted by the child, while callers that know the subclass can select specialized behavior.

This design fits scenarios where an abstract manager returns a handle and certain implementations allow optional flags (known by their direct callers) to influence the kind of handle returned. Generic code doesn’t know about those flags and doesn’t need to; it simply calls the no-argument method as promised by the interface.

from abc import ABC, abstractmethod
class LinkProvider(ABC):
    @abstractmethod
    def connect(self):
        raise NotImplementedError()
class DefaultLinkProvider(LinkProvider):
    def connect(self, *, thread_safe: bool = False):
        if thread_safe:
            return SafeHandle()
        else:
            return PlainHandle()

Here the abstract method accepts no arguments, and the override accepts everything the base does. Callers that only depend on the abstract interface can invoke connect() without worrying about flags. Callers that know they’re dealing with DefaultLinkProvider may opt into thread_safe.

If you must keep a permissive abstract signature like (*args, **kwargs), the override should not require new mandatory positional parameters. One way to remain type-correct is to ensure any additional parameters introduced by the override are optional so that calls valid for the base remain valid for the derived implementation.

Why this matters

Respecting the original method contract makes code safer to reuse and reason about. It preserves substitutability, avoids brittle APIs, and keeps room for specialization without creating implicit breaking changes. It also aligns with the expectation that argument acceptance widens rather than narrows in derived classes. Even if your runtime or toolchain lets a stricter override slip through, the design cost shows up later—in surprising errors or hard-to-use abstractions.

Takeaways

Narrowing a method’s accepted inputs in a subclass is a bad trade-off, even if it executes. Keep the base method’s signature as tight as its contract requires, and broaden in the subclass only in ways that do not invalidate calls allowed by the base. For feature flags and specialized tuning, prefer optional, keyword-only parameters in the override so that the generic interface remains fully honored while advanced callers can opt in to specific behavior. When in doubt, check that your design satisfies the Liskov Substitution Principle and remember the guiding idea: method arguments are contravariant.