copy.replace: _SupportsReplace[RT] breaks Self-returning __replace__ with bound TypeVar

Aperta
#15,973 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
55/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Tranquilla
Stack tecnologico
python
Ambito
devtools

Direzione di ricerca

Individua il punto di ingresso dello stub di typeshed per copy.replace e riproduci con mypy l’esempio di bounded-TypeVar dell’issue. Confronta la firma attuale con il precedente approccio basato su bound-TypeVar descritto qui, quindi verifica che il caso Self-returning preservi T, considerando l’edge case indicato da Box[int] a Box[str]. Il lavoro è completato quando passano i controlli assert_type e return mostrati.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

PR #14786 changed copy.replace from a bound-TypeVar signature to a covariant protocol. This introduced a regression for __replace__ methods that return Self (dataclasses, namedtuples, etc.) when the argument is a bounded TypeVar.

To Reproduce

import copy
from dataclasses import dataclass
from typing import TypeVar, assert_type

@dataclass(frozen=True)
class BaseConfig:
    pg_ssl_key: str | None = None

@dataclass(frozen=True)
class SubConfig(BaseConfig):
    pg_host: str = "localhost"

T = TypeVar("T", bound=BaseConfig)

def f(config: T) -> T:
    result = copy.replace(config, pg_ssl_key="replaced")
    assert_type(result, T)
    return result

Playground: https://mypy-play.net/?mypy=master&python=3.14&gist=32a94d730b07faeb680192a3fdad1b0f

Expected

assert_type(result, T) passes — copy.replace returns the same type as the argument.

Actual

error: Expression is of type "BaseConfig", not "T"  [assert-type]
error: Incompatible return value type (got "BaseConfig", expected "T")  [return-value]

Concrete subclass works fine — copy.replace(SubConfig(), ...) correctly returns SubConfig. Only the TypeVar-bounded case breaks.

Root Cause

The change from:

_SR = TypeVar("_SR", bound=_SupportsReplace)
class _SupportsReplace(Protocol):
    def __replace__(self, ...) -> Self: ...
def replace(obj: _SR, /, ...) -> _SR: ...

to:

_RT_co = TypeVar("_RT_co", covariant=True)
class _SupportsReplace(Protocol[_RT_co]):
    def __replace__(self, ...) -> _RT_co: ...
def replace(obj: _SupportsReplace[_RT_co], /, ...) -> _RT_co: ...

The bound-TypeVar approach preserves the argument type through the call. The covariant protocol infers _RT_co from the protocol method return type, which for Self resolves at the upper bound level — losing the TypeVar.

This is a fundamental limitation of Python's structural subtyping: one signature cannot simultaneously express both "return same type as argument" (the common Self case) and "return whatever __replace__ declares" (the Box[int] -> Box[str] edge case).

The common case (dataclasses / namedtuples returning Self) should take priority, since the edge case can use typing.cast as a workaround but the Self case has no workaround.

Related

Lingua principale
Python
Stelle
5.1k
Fork
2.1k
Merge medio
1g 13h
PR unite (30g)
73

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di python/typeshed

Tutte le issue di python/typeshed

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.