Skip to content

MutableMapping.update override rejected when using Protocol #2693

@jab

Description

@jab

Summary

ty incorrectly rejects a valid method override of MutableMapping.update that mypy accepts. The override uses a Protocol-based type alias that is structurally compatible with the base class signature.

Reproduction

from __future__ import annotations
from collections.abc import MutableMapping, Iterator, Iterable
import typing as t

KT = t.TypeVar('KT')
VT = t.TypeVar('VT')
VT_co = t.TypeVar('VT_co', covariant=True)

@t.runtime_checkable
class Maplike(t.Protocol[KT, VT_co]):
    """Equivalent to typeshed's SupportsKeysAndGetItem."""
    def keys(self) -> Iterable[KT]: ...
    def __getitem__(self, key: KT, /) -> VT_co: ...

MapOrItems = Maplike[KT, VT] | Iterable[tuple[KT, VT]]

class MyMapping(MutableMapping[KT, VT]):
    _data: dict[KT, VT]

    def __init__(self) -> None:
        self._data = {}

    def __getitem__(self, key: KT) -> VT:
        return self._data[key]

    def __setitem__(self, key: KT, value: VT) -> None:
        self._data[key] = value

    def __delitem__(self, key: KT) -> None:
        del self._data[key]

    def __iter__(self) -> Iterator[KT]:
        return iter(self._data)

    def __len__(self) -> int:
        return len(self._data)

    # This signature accepts the same input types as MutableMapping.update
    def update(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None:
        pass

Expected Behavior

The update override should be accepted because:

  1. Maplike[KT, VT] is a Protocol with keys() and __getitem__ - structurally equivalent to SupportsKeysAndGetItem
  2. MapOrItems = Maplike[KT, VT] | Iterable[tuple[KT, VT]] covers all the argument types that MutableMapping.update accepts
  3. The **kw: VT parameter mirrors the kwargs overloads

mypy 1.19.1 accepts this with --strict.

Actual Behavior

error[invalid-method-override]: Invalid override of method `update`
    --> repro.py:39:9
     |
  37 |         return len(self._data)
  38 |
  39 |     def update(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None:
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `MutableMapping.update`
  40 |         pass
     |
info: This violates the Liskov Substitution Principle

Analysis

The MutableMapping.update signature in typeshed is:

@overload
def update(self, m: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...
@overload
def update(self: SupportsGetItem[str, _VT], m: SupportsKeysAndGetItem[str, _VT], /, **kwargs: _VT) -> None: ...
@overload
def update(self, m: Iterable[tuple[_KT, _VT]], /) -> None: ...
@overload
def update(self: SupportsGetItem[str, _VT], m: Iterable[tuple[str, _VT]], /, **kwargs: _VT) -> None: ...
@overload
def update(self: SupportsGetItem[str, _VT], /, **kwargs: _VT) -> None: ...

The child signature (self, arg: Maplike[KT, VT] | Iterable[tuple[KT, VT]] = (), /, **kw: VT) -> None is a valid override because:

  • Maplike is structurally equivalent to SupportsKeysAndGetItem
  • The union covers both main overload variants
  • Default () is compatible with Iterable[tuple[KT, VT]]
  • The kwargs match

ty should recognize this structural Protocol equivalence when checking method overrides.

Environment

  • ty 0.0.14
  • Python 3.13
  • mypy 1.19.1 (for comparison - accepts this code)

Note: This issue affects my bidict library, which passes mypy strict mode but fails ty checks on this signature.

Version

ty 0.0.14 (16597f5 2026-01-26)

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions