python / python/typeshed

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

Đang mở
#15,973 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
Python
Star
5.1k
Fork
2.1k
Merge trung bình
1 ngày 19 giờ
Pull request đã merge (30 ngày)
82

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Xác định entry point của stub typeshed cho copy.replace và tái hiện ví dụ bounded-TypeVar trong issue bằng mypy. So sánh signature hiện tại với cách tiếp cận trước đây dựa trên bound-TypeVar được mô tả ở đây, sau đó xác minh rằng trường hợp Self-returning vẫn giữ nguyên T, đồng thời xem xét edge case đã nêu từ Box[int] sang Box[str]. Được xem là hoàn tất khi các kiểm tra assert_type và return được hiển thị đều đạt.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
devtools
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
55/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.