python / python/typeshed

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

Abierto
#15,973 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Lenguaje dominante
Python
Estrellas
5.1k
Forks
2.1k
Merge medio
1 d 19 h
PR fusionados (30 d)
82

Descripción

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

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Localiza el punto de entrada del stub de typeshed para copy.replace y reproduce con mypy el ejemplo de bounded-TypeVar del issue. Compara la firma actual con el enfoque anterior basado en bound-TypeVar descrito aquí y, después, verifica que el caso Self-returning conserve T, teniendo en cuenta el edge case indicado de Box[int] a Box[str]. Se considera terminado cuando pasan las comprobaciones de assert_type y return mostradas.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
python
Área
devtools
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Tranquilo
Claridad
Bastante claro
Aptitud para principiantes
55/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.