You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
copy.replace type inference regression: breaks Self-returning __replace__ with bound TypeVar #21672
typeshed PR python/typeshed#14786 (synced into mypy) changed copy.replace from a bound-TypeVar signature to a covariant protocol. This breaks copy.replace for __replace__ methods that return Self (dataclasses, namedtuples, etc.) when the argument is a bounded TypeVar.
The bound-TypeVar approach preserved the argument type; the covariant protocol loses it because _RT_co is inferred from the protocol method return type which, for Self, resolves at the upper bound level.
Note
The motivation for python/typeshed#14786 was mypy issue #19694 which is about dataclasses.replace (not copy.replace), so the PR did not actually address its intended target.
changed the title [-]copy.replace type inference regression: breaks Self-returning __replace__ with bound TypeVar[/-][+]`copy.replace` type inference regression: breaks Self-returning __replace__ with bound TypeVar[/+]on Jul 3, 2026
This post was authored by claude-opus-5-5 on behalf of @JPHutchins. While working out JPHutchins/salix#359, JP asked me to report the upstream causes with evidence; this adds the union cases to this issue.
The union siblings of this issue: in mypy 2.4.0, copy.replace on a union of dataclasses returns object, and so does a TypeVar bound to a union. That's a join, which is wider than this issue's bound. pyright 1.1.414 and ty 0.0.90 keep the argument's type in both cases.
Measured with mypy --strict --python-version 3.14 and mypy 2.4.0's bundled typeshed, which includes Make copy.replace more strongly typed typeshed#14819. Each call reports Incompatible return value type (got "object", ...).
A second, never-matching copy.replace overload doesn't help when the whole union matches the first overload, as this identical-field A | B does. It does help when each member has a field the others lack: mypy then splits the union and returns it unwidened. The details, and the same rejection in pyright and ty, are in copy.replace rejects a union whose members each have a field the others lack (since #14819) typeshed#16582.
Activity
[-]copy.replace type inference regression: breaks Self-returning __replace__ with bound TypeVar[/-][+]`copy.replace` type inference regression: breaks Self-returning __replace__ with bound TypeVar[/+]trim21 commentedon Aug 9, 2026
relative: python/typeshed#15973 python/typeshed#15997
test: Seeded keeps its own __post_init__ beside SeededByDefault's (#354)
JPHutchins commentedon Oct 21, 2026
Warning
LLM Disclosure
This post was authored by claude-opus-5-5 on behalf of @JPHutchins. While working out JPHutchins/salix#359, JP asked me to report the upstream causes with evidence; this adds the union cases to this issue.
The union siblings of this issue: in mypy 2.4.0,
copy.replaceon a union of dataclasses returnsobject, and so does a TypeVar bound to a union. That's a join, which is wider than this issue's bound. pyright 1.1.414 and ty 0.0.90 keep the argument's type in both cases.Measurements and the mypy issues behind them
mypy --strict --python-version 3.14and mypy 2.4.0's bundled typeshed, which includes Makecopy.replacemore strongly typed typeshed#14819. Each call reportsIncompatible return value type (got "object", ...).__iter__method that returnsselfleads to variable being silently inferred asAny#14819. I ran the signature from Refine thecopy._SupportsReplace.__replace__signature typeshed#14786 (covariant_SupportsReplace[_RT_co],**changes: Any) as a local stub: mypy 2.4.0 returnsobjectfor both functions there too._RT_cofrom the union by join, which is abs(Union[int, Decimal]) is inferred as object #8601 forSupportsAbs. (🎁) Perform 'union math' on non overloaded calls #13493 ("union math" on non-overloaded calls) might fix it.copy.replaceoverload doesn't help when the whole union matches the first overload, as this identical-fieldA | Bdoes. It does help when each member has a field the others lack: mypy then splits the union and returns it unwidened. The details, and the same rejection in pyright and ty, are incopy.replacerejects a union whose members each have a field the others lack (since #14819) typeshed#16582.