From 58fe50be604e88f84f775dd0fc0ab049d61f104b Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 16 Jan 2026 19:56:37 +0100 Subject: [PATCH 1/3] allow disjoint types in Mapping.get MutableMapping.pop, dict.get, dict.pop --- .../@tests/test_cases/builtins/check_dict.py | 38 +++++++++++++------ stdlib/builtins.pyi | 15 +------- stdlib/typing.pyi | 20 ++++------ 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/stdlib/@tests/test_cases/builtins/check_dict.py b/stdlib/@tests/test_cases/builtins/check_dict.py index fe74ad49408e..52e3dc1c251d 100644 --- a/stdlib/@tests/test_cases/builtins/check_dict.py +++ b/stdlib/@tests/test_cases/builtins/check_dict.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Any, Dict, Generic, Iterable, Mapping, TypeVar, Union +from typing import Any, Dict, Generic, Iterable, Literal, Mapping, TypeVar, Union from typing_extensions import Self, assert_type ################################################################### @@ -74,14 +74,14 @@ def test_iterable_tuple_overload(x: Iterable[tuple[int, str]]) -> dict[int, str] assert_type(d_any.get("key"), Union[Any, None]) assert_type(d_any.get("key", None), Union[Any, None]) assert_type(d_any.get("key", any_value), Any) -assert_type(d_any.get("key", str_value), Any) -assert_type(d_any.get("key", int_value), Any) +assert_type(d_any.get("key", str_value), Union[Any, str]) +assert_type(d_any.get("key", int_value), Union[Any, int]) assert_type(d_str["key"], str) assert_type(d_str.get("key"), Union[str, None]) assert_type(d_str.get("key", None), Union[str, None]) # Pyright has str instead of Any here -assert_type(d_str.get("key", any_value), Any) # pyright: ignore[reportAssertTypeFailure] +assert_type(d_str.get("key", any_value), Union[str, Any]) assert_type(d_str.get("key", str_value), str) assert_type(d_str.get("key", int_value), Union[str, int]) @@ -89,20 +89,34 @@ def test_iterable_tuple_overload(x: Iterable[tuple[int, str]]) -> dict[int, str] result: str result = d_any["key"] result = d_any.get("key") # type: ignore[assignment] -result = d_any.get("key", None) # type: ignore[assignment] +# FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here +result = d_any.get("key", None) # type: ignore result = d_any.get("key", any_value) result = d_any.get("key", str_value) -result = d_any.get("key", int_value) +# FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here +result = d_any.get("key", int_value) # type: ignore result = d_str["key"] result = d_str.get("key") # type: ignore[assignment] -result = d_str.get("key", None) # type: ignore[assignment] -# Pyright has str | None here, see https://github.com/microsoft/pyright/discussions/9570 -result = d_str.get("key", any_value) # pyright: ignore[reportAssignmentType] +# FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here +result = d_str.get("key", None) # type: ignore +result = d_str.get("key", any_value) result = d_str.get("key", str_value) result = d_str.get("key", int_value) # type: ignore[arg-type] +def test_get_literal(d: dict[Literal["foo", "bar"], int], dynamic_key: str) -> None: + # Note: annotations also allow using keys of a disjoint type (e.g., int), + # linters / type checkers are free to issue warnings in such cases. + # statically, a .get(arg) is superfluous if the intersection of the + # dict key type and the argument type is empty. + # So we only test a case with non-empty intersection here. + + # check that dict wth Literal keys can get/pop a string key. + d.get(dynamic_key) + d.pop(dynamic_key) + + # Return values also make things weird # Pyright doesn't have a version of no-any-return, @@ -140,11 +154,13 @@ def test8() -> str: def test9() -> str: - return d_str.get("key", None) # type: ignore[return-value] + # FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[return-value] here + return d_str.get("key", None) # type: ignore def test10() -> str: - return d_str.get("key", any_value) # type: ignore[no-any-return] + # OK, return is Union[str, Any] + return d_str.get("key", any_value) def test11() -> str: diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 693dd0b77087..e71c10a08086 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -1218,19 +1218,8 @@ class dict(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... - # Positional-only in dict, but not in MutableMapping - @overload # type: ignore[override] - def get(self, key: _KT, default: None = None, /) -> _VT | None: ... - @overload - def get(self, key: _KT, default: _VT, /) -> _VT: ... - @overload - def get(self, key: _KT, default: _T, /) -> _VT | _T: ... - @overload - def pop(self, key: _KT, /) -> _VT: ... - @overload - def pop(self, key: _KT, default: _VT, /) -> _VT: ... - @overload - def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + # get: inherited from Mapping + # pop: inherited from MutableMapping def __len__(self) -> int: ... def __getitem__(self, key: _KT, /) -> _VT: ... def __setitem__(self, key: _KT, value: _VT, /) -> None: ... diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index 5521055bdef3..82f6aae50bdc 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -773,12 +773,10 @@ class Mapping(Collection[_KT], Generic[_KT, _VT_co]): @abstractmethod def __getitem__(self, key: _KT, /) -> _VT_co: ... # Mixin methods - @overload - def get(self, key: _KT, /) -> _VT_co | None: ... - @overload - def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter - @overload - def get(self, key: _KT, default: _T, /) -> _VT_co | _T: ... + @overload # intentionally positional-only + def get(self, key: Any, /) -> _VT_co | None: ... + @overload # intentionally positional-only + def get(self, key: Any, default: _T, /) -> _VT_co | _T: ... def items(self) -> ItemsView[_KT, _VT_co]: ... def keys(self) -> KeysView[_KT]: ... def values(self) -> ValuesView[_VT_co]: ... @@ -791,12 +789,10 @@ class MutableMapping(Mapping[_KT, _VT]): @abstractmethod def __delitem__(self, key: _KT, /) -> None: ... def clear(self) -> None: ... - @overload - def pop(self, key: _KT, /) -> _VT: ... - @overload - def pop(self, key: _KT, default: _VT, /) -> _VT: ... - @overload - def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + @overload # intentionally positional-only + def pop(self, key: Any, /) -> _VT | None: ... + @overload # intentionally positional-only + def pop(self, key: Any, default: _T, /) -> _VT | _T: ... def popitem(self) -> tuple[_KT, _VT]: ... # This overload should be allowed only if the value type is compatible with None. # From dcffb9ff035aca3774362f51714b57bd7cc049f6 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 16 Jan 2026 20:53:56 +0100 Subject: [PATCH 2/3] reverted Mapping.get changes, lets first try dict only --- stdlib/builtins.pyi | 10 ++++++++-- stdlib/collections/__init__.pyi | 17 +++++++++-------- stdlib/typing.pyi | 20 ++++++++++++-------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index e71c10a08086..9d843f6a59c1 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -1218,8 +1218,14 @@ class dict(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... - # get: inherited from Mapping - # pop: inherited from MutableMapping + @overload + def get(self, key: object, /) -> _VT | None: ... + @overload + def get(self, key: object, default: _T, /) -> _VT | _T: ... + @overload # type: ignore[override] + def pop(self, key: object, /) -> _VT | None: ... + @overload + def pop(self, key: object, default: _T, /) -> _VT | _T: ... def __len__(self) -> int: ... def __getitem__(self, key: _KT, /) -> _VT: ... def __setitem__(self, key: _KT, value: _VT, /) -> None: ... diff --git a/stdlib/collections/__init__.pyi b/stdlib/collections/__init__.pyi index 8636e6cdbdc3..e0d795170f6b 100644 --- a/stdlib/collections/__init__.pyi +++ b/stdlib/collections/__init__.pyi @@ -105,12 +105,15 @@ class UserDict(MutableMapping[_KT, _VT]): @overload def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... if sys.version_info >= (3, 12): + # UserDict allows key and default as keyword arguments @overload - def get(self, key: _KT, default: None = None) -> _VT | None: ... + def get(self, key: object) -> _VT | None: ... @overload - def get(self, key: _KT, default: _VT) -> _VT: ... + def get(self, key: object, default: _T) -> _VT | _T: ... + @overload # type: ignore[override] + def pop(self, key: object) -> _VT | None: ... @overload - def get(self, key: _KT, default: _T) -> _VT | _T: ... + def pop(self, key: object, default: _T) -> _VT | _T: ... class UserList(MutableSequence[_T]): data: list[_T] @@ -381,12 +384,10 @@ class OrderedDict(dict[_KT, _VT]): @overload def setdefault(self, key: _KT, default: _VT) -> _VT: ... # Same as dict.pop, but accepts keyword arguments + @overload # type: ignore[override] + def pop(self, key: object) -> _VT | None: ... @overload - def pop(self, key: _KT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _VT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _T) -> _VT | _T: ... + def pop(self, key: object, default: _T) -> _VT | _T: ... def __eq__(self, value: object, /) -> bool: ... @overload def __or__(self, value: dict[_KT, _VT], /) -> Self: ... diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index 82f6aae50bdc..5521055bdef3 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -773,10 +773,12 @@ class Mapping(Collection[_KT], Generic[_KT, _VT_co]): @abstractmethod def __getitem__(self, key: _KT, /) -> _VT_co: ... # Mixin methods - @overload # intentionally positional-only - def get(self, key: Any, /) -> _VT_co | None: ... - @overload # intentionally positional-only - def get(self, key: Any, default: _T, /) -> _VT_co | _T: ... + @overload + def get(self, key: _KT, /) -> _VT_co | None: ... + @overload + def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter + @overload + def get(self, key: _KT, default: _T, /) -> _VT_co | _T: ... def items(self) -> ItemsView[_KT, _VT_co]: ... def keys(self) -> KeysView[_KT]: ... def values(self) -> ValuesView[_VT_co]: ... @@ -789,10 +791,12 @@ class MutableMapping(Mapping[_KT, _VT]): @abstractmethod def __delitem__(self, key: _KT, /) -> None: ... def clear(self) -> None: ... - @overload # intentionally positional-only - def pop(self, key: Any, /) -> _VT | None: ... - @overload # intentionally positional-only - def pop(self, key: Any, default: _T, /) -> _VT | _T: ... + @overload + def pop(self, key: _KT, /) -> _VT: ... + @overload + def pop(self, key: _KT, default: _VT, /) -> _VT: ... + @overload + def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... def popitem(self) -> tuple[_KT, _VT]: ... # This overload should be allowed only if the value type is compatible with None. # From 5e59debae69b9933e0d99bdb53d8560819aa1365 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 16 Jan 2026 21:59:05 +0100 Subject: [PATCH 3/3] try without overload (using defaul=None) --- stdlib/@tests/test_cases/builtins/check_dict.py | 12 ++++++++---- stdlib/builtins.pyi | 11 +++-------- stdlib/collections/__init__.pyi | 12 ++++++------ stdlib/importlib/metadata/__init__.pyi | 6 ++---- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/stdlib/@tests/test_cases/builtins/check_dict.py b/stdlib/@tests/test_cases/builtins/check_dict.py index 52e3dc1c251d..405993bbf95b 100644 --- a/stdlib/@tests/test_cases/builtins/check_dict.py +++ b/stdlib/@tests/test_cases/builtins/check_dict.py @@ -88,7 +88,8 @@ def test_iterable_tuple_overload(x: Iterable[tuple[int, str]]) -> dict[int, str] # Now with context! result: str result = d_any["key"] -result = d_any.get("key") # type: ignore[assignment] +# FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here +result = d_any.get("key") # type: ignore # FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here result = d_any.get("key", None) # type: ignore result = d_any.get("key", any_value) @@ -97,7 +98,8 @@ def test_iterable_tuple_overload(x: Iterable[tuple[int, str]]) -> dict[int, str] result = d_any.get("key", int_value) # type: ignore result = d_str["key"] -result = d_str.get("key") # type: ignore[assignment] +# FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here +result = d_str.get("key") # type: ignore # FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[assignment] here result = d_str.get("key", None) # type: ignore result = d_str.get("key", any_value) @@ -126,7 +128,8 @@ def test_get_literal(d: dict[Literal["foo", "bar"], int], dynamic_key: str) -> N def test2() -> str: - return d_any.get("key") # type: ignore[return-value] + # FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[return-value] here + return d_any.get("key") # type: ignore # def test3() -> str: @@ -150,7 +153,8 @@ def test7() -> str: def test8() -> str: - return d_str.get("key") # type: ignore[return-value] + # FIXME: https://github.com/python/mypy/issues/20576 prevents using ignore[return-value] here + return d_str.get("key") # type: ignore def test9() -> str: diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 9d843f6a59c1..356551b44f00 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -79,6 +79,7 @@ if sys.version_info >= (3, 14): from _typeshed import AnnotateFunc _T = TypeVar("_T") +_N = TypeVar("_N", default=None) _I = TypeVar("_I", default=int) _T_co = TypeVar("_T_co", covariant=True) _T_contra = TypeVar("_T_contra", contravariant=True) @@ -1218,14 +1219,8 @@ class dict(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... - @overload - def get(self, key: object, /) -> _VT | None: ... - @overload - def get(self, key: object, default: _T, /) -> _VT | _T: ... - @overload # type: ignore[override] - def pop(self, key: object, /) -> _VT | None: ... - @overload - def pop(self, key: object, default: _T, /) -> _VT | _T: ... + def get(self, key: object, default: _N = None, /) -> _VT | _N: ... + def pop(self, key: object, default: _N = None, /) -> _VT | _N: ... def __len__(self) -> int: ... def __getitem__(self, key: _KT, /) -> _VT: ... def __setitem__(self, key: _KT, value: _VT, /) -> None: ... diff --git a/stdlib/collections/__init__.pyi b/stdlib/collections/__init__.pyi index e0d795170f6b..e806e01ef231 100644 --- a/stdlib/collections/__init__.pyi +++ b/stdlib/collections/__init__.pyi @@ -107,13 +107,13 @@ class UserDict(MutableMapping[_KT, _VT]): if sys.version_info >= (3, 12): # UserDict allows key and default as keyword arguments @overload - def get(self, key: object) -> _VT | None: ... - @overload def get(self, key: object, default: _T) -> _VT | _T: ... - @overload # type: ignore[override] - def pop(self, key: object) -> _VT | None: ... + @overload + def get(self, key: object, default: None = None) -> _VT | None: ... @overload def pop(self, key: object, default: _T) -> _VT | _T: ... + @overload # type: ignore[override] + def pop(self, key: object, default: None = None) -> _VT | None: ... class UserList(MutableSequence[_T]): data: list[_T] @@ -384,10 +384,10 @@ class OrderedDict(dict[_KT, _VT]): @overload def setdefault(self, key: _KT, default: _VT) -> _VT: ... # Same as dict.pop, but accepts keyword arguments - @overload # type: ignore[override] - def pop(self, key: object) -> _VT | None: ... @overload def pop(self, key: object, default: _T) -> _VT | _T: ... + @overload # type: ignore[override] + def pop(self, key: object, default: None = None) -> _VT | None: ... def __eq__(self, value: object, /) -> bool: ... @overload def __or__(self, value: dict[_KT, _VT], /) -> Self: ... diff --git a/stdlib/importlib/metadata/__init__.pyi b/stdlib/importlib/metadata/__init__.pyi index 9286e92331c8..e66c0c3c5faf 100644 --- a/stdlib/importlib/metadata/__init__.pyi +++ b/stdlib/importlib/metadata/__init__.pyi @@ -165,11 +165,9 @@ if sys.version_info >= (3, 10) and sys.version_info < (3, 12): class Deprecated(Generic[_KT, _VT]): def __getitem__(self, name: _KT) -> _VT: ... @overload - def get(self, name: _KT, default: None = None) -> _VT | None: ... + def get(self, key: object, default: _T) -> _VT | _T: ... @overload - def get(self, name: _KT, default: _VT) -> _VT: ... - @overload - def get(self, name: _KT, default: _T) -> _VT | _T: ... + def get(self, key: object, default: None = None) -> _VT | None: ... def __iter__(self) -> Iterator[_KT]: ... def __contains__(self, *args: object) -> bool: ... def keys(self) -> dict_keys[_KT, _VT]: ...