Skip to content

False positive on override against IO[str] subclass (overload-selection picks the IO[bytes] overload) #3380

@tdhopper

Description

@tdhopper

Summary

Summary

ty raises invalid-method-override on a class that subclasses IO[str] and overrides write / writelines. The diagnostic says the base parameter is Buffer / Iterable[Buffer], but for IO[str] the typeshed overloads should resolve AnyStr to str. mypy and pyright are silent on the same file.

Minimal repro

repro.py:

from typing import IO, Iterable


class NullFile(IO[str]):
    def write(self, s: str, /) -> int:
        return 0

    def writelines(self, lines: Iterable[str], /) -> None:
        pass

Command

ty check repro.py

ty output

error[invalid-method-override]: Invalid override of method `write`
    --> repro.py:5:9
     |
   5 |     def write(self, s: str, /) -> int:
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `IO.write`
     |
    ::: stdlib/typing.pyi:1951:9
     |
1951 |     def write(self, s: AnyStr, /) -> int: ...
     |         -------------------------------- `IO.write` defined here
     |
info: parameter `s` has an incompatible type: `Buffer` is not assignable to `str`
info: This violates the Liskov Substitution Principle

error[invalid-method-override]: Invalid override of method `writelines`
    --> repro.py:8:9
     |
   8 |     def writelines(self, lines: Iterable[str], /) -> None:
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `IO.writelines`
     |
    ::: stdlib/typing.pyi:1957:9
     |
1957 |     def writelines(self, lines: Iterable[AnyStr], /) -> None: ...
     |         ---------------------------------------------------- `IO.writelines` defined here
     |
info: parameter `lines` has an incompatible type: `Iterable[Buffer]` is not assignable to `Iterable[str]`
info: └── protocol `Iterable[Buffer]` is not assignable to protocol `Iterable[str]`
info:     └── incompatible return types: `Iterator[Buffer]` is not assignable to `Iterator[str]`
info:         └── protocol `Iterator[Buffer]` is not assignable to protocol `Iterator[str]`
info:             └── incompatible return types: `Buffer` is not assignable to `str`
info: This violates the Liskov Substitution Principle

Found 2 diagnostics

Cross-checks

  • mypy --strict repro.py: Success: no issues found in 1 source file
  • pyright repro.py (1.1.408): 0 errors, 0 warnings, 0 informations

Typeshed context

IO.write and IO.writelines are defined with two overloads each in typing.pyi:

class IO(Generic[AnyStr]):
    @overload
    def write(self: IO[bytes], s: ReadableBuffer, /) -> int: ...
    @overload
    def write(self, s: AnyStr, /) -> int: ...

    @overload
    def writelines(self: IO[bytes], lines: Iterable[ReadableBuffer], /) -> None: ...
    @overload
    def writelines(self, lines: Iterable[AnyStr], /) -> None: ...

_typeshed.ReadableBuffer is an alias for collections.abc.Buffer, which matches the Buffer ty reports. For a subclass of IO[str] the override should be checked against the second overload with AnyStr = str, giving s: str and lines: Iterable[str]. Something is letting Buffer leak into the comparison instead.

ty's diagnostic source pointer for IO.writelines lands on line 1957 (the generic overload), so the issue is not as simple as "ty picked the bytes overload", but the materialized parameter type the override is being checked against still comes out as Buffer.

Possibly related

Versions

  • ty 0.0.33 (c512d84 2026-04-28)
  • Python 3.14.3

Version

No response

Metadata

Metadata

Assignees

Labels

genericsBugs or features relating to ty's generics implementation

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions