Skip to content

Ignore plugin-generated members when inferring PEP 695 variance#21564

Draft
allanlewis wants to merge 1 commit into
python:masterfrom
allanlewis:attrs-pep695-variance-inference
Draft

Ignore plugin-generated members when inferring PEP 695 variance#21564
allanlewis wants to merge 1 commit into
python:masterfrom
allanlewis:attrs-pep695-variance-inference

Conversation

@allanlewis
Copy link
Copy Markdown

@allanlewis allanlewis commented May 29, 2026

Important

AI / LLM disclosure. This pull request was produced by an AI coding agent — Anthropic's Claude Code (model Claude Opus 4.8). The diff, the added tests, and this description were generated in their entirety by the model. I have reviewed the change and take responsibility for it, but want to be fully transparent that I did not write it by hand.

I'm aware of the CONTRIBUTING.md guidance on LLM-assisted contributions, in particular that mypy discourages use of LLMs by new contributors and that PRs from new contributors that are mostly LLM-generated may be closed. As a first-time contributor I didn't want to spring this on anyone unannounced, so I opened a Gitter thread to discuss it first, and I'm filing this as a draft: https://matrix.to/#/!FjKyUUyKpKFUvCNnMz:gitter.im/$NQqpiczcc57aIWYrrmkPXWl6GFA7cD4gZhgtKPslYS0?via=gitter.im
If this isn't a welcome way to contribute, please feel free to close it.

What

PEP 695 variance inference treats generic @attrs.define / @attrs.frozen classes as invariant — and empty ones as contravariant — even when the type variable is used only covariantly. Removing the decorator makes the same class infer covariant, as expected.

@attrs.frozen
class Box[T]:
    def get(self) -> T: ...

bi: Box[int]
bo: Box[object] = bi  # error today; should be allowed (Box is covariant)

Why

infer_variance (mypy/subtypes.py) iterates over every member from all_non_object_members, including members synthesized by plugins. Two attrs-generated members reuse the class's own type and skew inference:

Synthesized member Type after self-binding Effect on T
__lt__ / __le__ / __gt__ / __ge__ (self, other: Self[T]) -> bool contravariant T
__attrs_attrs__ (with a T-typed field) ClassVar[tuple[Attribute[T], ...]]; Attribute is invariant invariant T

The dataclass plugin's __replace__ (synthesized on Python 3.13+) has the same effect on frozen dataclasses with fields.

This is the same class of bug as #17783, which special-cased the dataclass plugin's internal __mypy-replace; attrs uses different member names, so that fix never covered it.

Fix

Skip members flagged plugin_generated during variance inference. A user's own fields and methods are never plugin_generated, so they continue to drive the result; only synthesized members are ignored. This subsumes the existing __mypy-replace special case (kept in place for clarity).

sym = info.get(member)
if sym is not None and sym.plugin_generated:
    continue

Tests

  • testAttrsPEP695InferVariance (check-plugin-attrs.test) — a covariant attrs class infers covariant; a mutable-field class stays invariant; a class with a user-written T-parameter method stays contravariant (confirming only synthesized members are skipped).
  • testPEP695InferVarianceInFrozenDataclassPython313 (check-python312.test) — a frozen dataclass stays covariant under --python-version 3.13.

Both fail without the change and pass with it; the full testcheck suite passes locally.

Relates to #17623.

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

infer_variance walked every member of a class, including methods and
attributes synthesized by plugins. Those synthesized members reuse the
class's own type in positions that don't reflect how the user uses the
type variable, which corrupted the inferred variance:

- attrs generates ordering methods (__lt__/__le__/__gt__/__ge__) whose
  ``other`` parameter is typed as the class's own Self[T], plus an
  __attrs_attrs__ tuple of the invariant Attribute[T]. This made
  @attrs.define/@attrs.frozen generic classes invariant (and empty ones
  contravariant) even when T was used only covariantly.
- On Python 3.13+ the dataclass plugin generates __replace__, whose
  keyword parameters reuse the field types and made otherwise covariant
  frozen dataclasses invariant.

User-written declarations are never flagged plugin_generated, so
skipping plugin-generated members during variance inference leaves real
fields and methods in control while ignoring synthesized ones. This
generalizes the existing __mypy-replace special case.

Tests:
- testPEP695InferVarianceWithAttrsFrozen (check-python312.test)
- testPEP695InferVarianceInFrozenDataclass (check-python313.test)

Relates to python#17623.

Assistant-Model: Claude Code
@allanlewis allanlewis force-pushed the attrs-pep695-variance-inference branch from 74e2000 to 4d6b6d1 Compare May 29, 2026 16:23
@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.

1 participant