Skip to content

Treat writable callable attributes as invariant in PEP 695 variance inference - #22438

Closed
OBOneProtocol wants to merge 1 commit into
python:masterfrom
OBOneProtocol:fix/issue-22339
Closed

OBOneProtocol wants to merge 1 commit into
python:masterfrom
OBOneProtocol:fix/issue-22339

Conversation

@OBOneProtocol

Copy link
Copy Markdown

Fixes #22339

infer_variance() uses the settable flag to make writable attributes invariant, but only in one direction. When T replaced by object gives a supertype of the member's type, it clears both contra and co; when it gives a non-subtype, it clears only co. A writable Callable[[T], None] attribute falls into the second case, so T stayed contravariant and Handler[Animal] was accepted where Handler[Dog] was expected, which fails at runtime once the callback is reassigned.

This adds the symmetric if settable: contra = False to that branch. A writable attribute is read and written, so it should constrain the type variable in both directions, which is what mypy already does for writable T and Callable[[], T] attributes and what the protocol checker requires for the same attribute shape.

The change is limited to the settable-member branch; it doesn't touch the find_member(...) call that #19466 changes.

Tests: testPEP695InferVarianceWritableCallableAttribute in check-python312.test covers the issue's example (now an arg-type error), a writable Callable[[T], None] attribute in both directions, a writable Callable[[], T] attribute (already invariant), and Final, underscore-prefixed and read-only property versions plus a methods-only class, which all stay contravariant. The new case fails on master and passes with the change. I also ran check-python312/313/314, protocols, generic-subtyping, dataclasses, typevar-defaults, final, generics, classes, selftype and incremental (2,000+ cases, no other changes), the self-check on mypy/subtypes.py, and black/ruff/codespell at the pre-commit versions.

One note: classes defining __setattr__ treat members as settable, so they may now infer invariant in more cases. No existing test changed; mypy_primer should show whether that matters in practice.


This change was investigated, implemented and tested with help from Claude Code.

…nference

infer_variance() only applied the extra "settable" constraint in one
direction: when the getter type was not a supertype of its object-expanded
form, a settable member also disabled covariance. The symmetric case was
missing, so a writable attribute such as `cb: Callable[[T], None]` (where T
appears only in an input position) was inferred contravariant, even though
writes make it an input position for the reversed direction as well. This
allowed Handler[Animal] where Handler[Dog] was expected, leading to runtime
AttributeErrors.

Now a settable member that rules out covariance also rules out
contravariance, making such attributes invariant. Final, underscore-prefixed
and read-only property members are unaffected and stay contravariant.

Fixes python#22339.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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.

PEP 695 variance inference treats a writable Callable[[T], None] attribute as contravariant.

2 participants