Skip to content

Make gzip.GzipFile inherit IO[bytes] to match BZ2File/LZMAFile - #21798

Closed
sarbojitrana wants to merge 1 commit into
python:masterfrom
sarbojitrana:fix-overload-callable-assignment
Closed

Make gzip.GzipFile inherit IO[bytes] to match BZ2File/LZMAFile#21798
sarbojitrana wants to merge 1 commit into
python:masterfrom
sarbojitrana:fix-overload-callable-assignment

Conversation

@sarbojitrana

Copy link
Copy Markdown

Fixes #21784

Root cause

from collections.abc import Callable
from typing import IO
import gzip

foo: Callable[[str, str], IO] = gzip.open  # false positive: Incompatible types in assignment

typing.IO is a nominal class, not a Protocol, so mypy's subtype check for "is this overloaded function assignable to this Callable type" (visit_overloaded in mypy/subtypes.py, the isinstance(right, CallableType) branch) requires each overload's return type to be an actual (nominal) subtype of IO, not just structurally compatible.

gzip.open has 4 overloads returning GzipFile, TextIOWrapper, or unions of the two. TextIOWrapper already inherits TextIO (→ IO[str]), but gzip.GzipFile only inherited from BaseStream (→ BufferedIOBaseIOBase), with no IO in its MRO at all. So none of gzip.open's overloads qualified, and the assignment was rejected.

By contrast, builtins.open's overloads all return classes that do inherit the matching IO variant (FileIO, BufferedReader/Writer/Random all inherit BinaryIO, and the final fallback overload returns IO[Any] directly), which is why foo: Callable[[str, str], IO] = open type-checks fine today — this is the asymmetry the issue reporter noticed.

Crucially, gzip.GzipFile's sibling classes in the same compression-module family, bz2.BZ2File and lzma.LZMAFile, are already declared as class BZ2File(BaseStream, IO[bytes]): / class LZMAFile(BaseStream, IO[bytes]):. GzipFile was simply missing the same IO[bytes] base — an inconsistency in mypy/typeshed/stdlib/gzip.pyi rather than a bug in mypy's core type-checking logic.

Fix

In mypy/typeshed/stdlib/gzip.pyi:

  • class GzipFile(BaseStream):class GzipFile(BaseStream, IO[bytes]): (matching BZ2File/LZMAFile)
  • Added from typing import IO, ...
  • mode: object needed a # type: ignore[assignment], since IO.mode is declared as a str property but GzipFile.mode is genuinely an int (READ/WRITE constants) at runtime — the same kind of unavoidable, deliberate mismatch already suppressed elsewhere in typeshed for other IO subclasses (e.g. the # type: ignore[misc] comments on FileIO/BufferedReader/BufferedWriter/BufferedRandom/TextIOWrapper/LZMAFile).

Proof / verification

Before the fix, both examples from the issue fail:

$ mypy --check-untyped-defs repro.py
repro.py:5: error: Incompatible types in assignment (expression has type overloaded function, variable has type "Callable[[str, str], IO[Any]]")  [assignment]

After the fix, the original repro is clean:

$ mypy --check-untyped-defs repro.py
Success: no issues found in 1 source file

Verification performed:

  • python -m mypy.stubtest gzip — passes, no new stub/runtime mismatches introduced (confirmed the 4 pre-existing io.Reader/io.Writer stubtest errors are unrelated and present identically before this change).
  • python -m mypy --config-file mypy_self_check.ini mypy — mypy's self-check passes clean.
  • Added a regression test testOverloadedGzipOpenAssignableToCallableReturningIO_Issue21784 to test-data/unit/pythoneval.test (this suite runs against the real stdlib stubs via CPython, matching how the issue actually manifests). Verified it fails without the fix and passes with it.
  • Full test suite (python -m pytest -q mypy/test/): 12391 passed, 360 skipped, 10 xfailed, with a single unrelated flaky failure (testAnyTypeAlias2, a ResourceWarning from subprocess cleanup under pytest-xdist, unrelated to typing/gzip) that passes in isolation and reproduces identically on master without this change.

Scope note

The issue's second comment shows a related-but-distinct scenario (assigning gzip.open and open to the same local variable across if/else branches) that still errors after this fix. That's a different code path — joining two Overloaded types when widening a variable's type across control-flow branches — and is unrelated to GzipFile's missing IO base. Left out of scope here to keep this change minimal and low-risk.

GzipFile only inherited from BaseStream, unlike its sibling classes
bz2.BZ2File and lzma.LZMAFile, which both additionally inherit
IO[bytes]. Since typing.IO is a nominal (non-protocol) class, mypy's
subtype check for "is this overloaded function assignable to this
Callable type" requires an actual inheritance relationship on the
return type. Without it, none of gzip.open's overloads (returning
GzipFile, TextIOWrapper, or unions of those) were considered
assignable to Callable[[str, str], IO], producing a false positive
"Incompatible types in assignment" error.

Fixes python#21784
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

@sarbojitrana

Copy link
Copy Markdown
Author

hi @JukkaL could you please review this pull request

@ilevkivskyi ilevkivskyi closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

false positive Incompatible types in assignment for overloaded function

2 participants