| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Warning
LLM Disclosure
This post was authored by claude-opus-5-5 on behalf of @JPHutchins. While working out JPHutchins/salix#359 (a dataclass_transform library whose replace stub has the same ParamSpec-protocol shape as copy.replace), JP asked me to report the upstream causes with evidence.
Since #14819 typed copy.replace through _SupportsReplace[_P, _RT_co], a union argument is rejected whenever each member has a field the others lack, such as Circle | Square sharing name.
Proposed fix: a second overload that no argument can match. Checkers then apply the spec's argument type expansion and check each union member on its own, and changes stay typed. Measured on all three: rename passes and wrong is still refused. mypy still returns object for bump, its join, linked below.
import copy
from dataclasses import dataclass
@dataclass(frozen=True)
class Circle:
name: str
radius: float
@dataclass(frozen=True)
class Square:
name: str
side: float
@dataclass(frozen=True)
class Left:
x: int
@dataclass(frozen=True)
class Right:
x: int
def rename(shape: Circle | Square) -> Circle | Square:
return copy.replace(shape, name="renamed")
def bump(value: Left | Right) -> Left | Right:
return copy.replace(value, x=1)
def wrong(shape: Circle | Square) -> Circle | Square:
return copy.replace(shape, name=1)| at a 3.14 target | rename | bump | wrong |
|---|---|---|---|
| ty 0.0.63, bundled, before #14819 | ok | ok | not checked |
| ty 0.0.90, bundled, with #14819 | rejected | ok | refused |
| pyright 1.1.414, bundled, before #14819 | ok | ok | not checked |
| pyright 1.1.414, main's signature | rejected | ok | refused |
| mypy 2.4.0, bundled, with #14819 | rejected, object | object | refused |
| all three, main's signature plus the overload below | ok | mypy: object; pyright and ty: ok | refused |
ty 0.0.90 (bundled copy.replace):
repro.py:28:25: error[invalid-argument-type] Argument to function `replace` is incorrect: Expected `_SupportsReplace[((*, name: str = ..., radius: float = ...)) | ((*, name: str = ..., side: float = ...)), Circle | Square]`, found `Circle | Square`
pyright 1.1.414, against main's signature (its bundled typeshed is 289e5d3, from 2026-06-24, so I put main's _SupportsReplace and replace in a local stub):
repro_tsmain.py:28:25 - error: Argument of type "Circle | Square" cannot be assigned to parameter "obj" of type "_SupportsReplace[_P@replace, _RT_co@replace]" in function "replace"
mypy 2.4.0 (bundled):
repro.py:28: error: Incompatible return value type (got "object", expected "Circle | Square") [return-value] repro.py:28: error: Argument 1 to "replace" has incompatible type "Circle | Square"; expected "_SupportsReplace[[DefaultNamedArg(str, 'name'), DefaultNamedArg(float, 'radius')], object]" [arg-type] repro.py:32: error: Incompatible return value type (got "object", expected "Left | Right") [return-value]
At run time on CPython 3.14.8, rename(Square("s", 1.0)) returns Square(name='renamed', side=1.0) and bump(Right(0)) returns Right(x=1).
The proposed overloads, and why they work@final
@type_check_only
class _Unreplaceable: ...
@overload
def replace(obj: _SupportsReplace[_P, _RT_co], /, *_: _P.args, **changes: _P.kwargs) -> _RT_co: ...
@overload
def replace(obj: _Unreplaceable, /, **changes: object) -> Never: ...From the typing spec's overload call evaluation, step 3:
If step 2 produces errors for all overloads, perform "argument type expansion". Union types can be expanded into their constituent subtypes.
If all argument lists evaluate successfully, combine their respective return types by union to determine the final return type for the call, and stop.
pyright's maintainer has explained why a ParamSpec can't bind a union of signatures, and has pointed to overloads for other ParamSpec-and-union shapes. Neither thread is about copy.replace:
| Back | FazBrowse Home | New Git URL |
Activity
JPHutchins commented on Oct 21, 2026
Warning
LLM Disclosure
This post was authored by claude-opus-5-5 on behalf of @JPHutchins. It adds a cost of the proposed overloads, measured after the issue was filed.
A cost of the proposed overloads: a wrong change on a single argument loses its specific error on mypy and ty. pyright keeps its specific message, and both other checkers still report an error, but a less useful one. With overloads, a call that fails every overload is reported as a whole.
So the overloads trade sharper messages on single-argument mistakes, the common case, for accepting unions whose members each have a field the others lack. JPHutchins/salix#359 measured the same trade in salix's own stub and chose to keep the precise messages, documenting a workaround for union call sites instead.