Skip to content

Liskov substitution paradox when inheriting from io.StringIO (and probably other io types) #12429

Closed as not planned
@solsword

Description

@solsword

I've just read through this bug:

#6076

which has finally answered my question, and I'm hoping that as an ugly partial fix, we could at least have a special message warning users what's going on if they ever try to inherit from io.StringIO and similar and override a method. Or if I'm wrong about the root issue here, I'd love to have a deeper discussion of what's going on.

To explain further: I've written a class that inherits from io.StringIO. Then I try to override a method (writelines in my case). I declare the argument type for my override as Iterable[str], and mypy complains this is Liskov-substitution-incompatible with the IOBase supertype which has this argument as an Iterable[Buffer]. So I go, "okay, fine, no big deal, I'll just import Buffer from typing_extensions and change it. But now I get the same complaint, this time about TextIOBase with argument type Iterable[str]. This... makes no sense to me, but after diving down various internet rabbit holes I finally come across the bug linked above which implies that it's in fact impossible to properly declare a type for that argument, due to the weirdness in IO stuff. I can't just follow the suggestion to use typing.IO here because I actually do need to inherit from io.StringIO for functionality (Is there a way to at-runtime inherit from one class but tell mypy we're actually a different class' subtype? I'm guessing not).

An example to reproduce this:

from typing import Iterable, Any
from typing_extensions import Buffer


class A(TextIOBase):
    def writelines(self, lines: Iterable[str]) -> None: # E: Argument 1 of "w…
        pass

class B(TextIOBase):
    def writelines(self, lines: Iterable[Buffer]) -> None: # E: Argument 1 of…
        pass

class C(TextIOBase):
    def writelines(self, lines: Iterable[Any]) -> None:
        pass

In this example, I get the following mypy output:

t.py:8: error: Argument 1 of "writelines" is incompatible with supertype "IOBase"; supertype defines the argument type as "Iterable[Buffer]"  [override]
t.py:8: note: This violates the Liskov substitution principle
t.py:8: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
t.py:12: error: Argument 1 of "writelines" is incompatible with supertype "TextIOBase"; supertype defines the argument type as "Iterable[str]"  [override]
t.py:12: note: This violates the Liskov substitution principle
t.py:12: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Found 2 errors in 1 file (checked 1 source file)

So neither A nor B works, and C is the closest reasonable thing I can do (I went with an Iterable[str] plus # type: ignore instead).

I realize the underlying issue may be intractable, but I'd have really appreciated a special note with my error messages letting me know that it's impossible to correctly type this, and linking to an explanation in the docs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions