python / python/typeshed

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

未关闭
#15,973 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

主要语言
Python
星标
5.1k
派生
2.1k
平均合并
1 天 19 小时
30 天内合并 PR
82

描述

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

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

定位 copy.replace 的 typeshed 存根入口点,并使用 mypy 重现 issue 中的 bounded-TypeVar 示例。将当前签名与此处描述的先前基于 bound-TypeVar 的方法进行比较,然后验证 Self-returning 情况会保留 T,同时考虑所述的从 Box[int] 到 Box[str] 的 edge case。显示的 assert_type 和 return 检查通过即表示完成。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
devtools
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
基本清楚
新手友好度
55/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。