Skip to content

copy.replace type inference regression: breaks Self-returning __replace__ with bound TypeVar #21672

Description

@trim21

Bug Report

typeshed PR python/typeshed#14786 (synced into mypy) changed copy.replace from a bound-TypeVar signature to a covariant protocol. This breaks copy.replace for __replace__ methods that return Self (dataclasses, namedtuples, etc.) when the argument is a bounded TypeVar.

To Reproduce

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

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

Expected Behavior

assert_type(result, T) should pass — copy.replace should return the same type as the argument.

Actual Behavior

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

mypy resolves _RT_co from _SupportsReplace.__replace__ return type, which for a Self-returning method resolves to the upper bound BaseConfig, not T.

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

Root Cause

typeshed PR python/typeshed#14786 changed copy.pyi 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 preserved the argument type; the covariant protocol loses it because _RT_co is inferred from the protocol method return type which, for Self, resolves at the upper bound level.

Note

The motivation for python/typeshed#14786 was mypy issue #19694 which is about dataclasses.replace (not copy.replace), so the PR did not actually address its intended target.

Your Environment

  • Mypy version used: 2.2.0+dev
  • Mypy command-line flags: --python-version 3.13
  • Python version used: 3.13+

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions