From 54b0beb4061a01ca8551c9403bc098b3c83d2055 Mon Sep 17 00:00:00 2001 From: Piotr Kaznowski Date: Sun, 19 Jul 2026 13:53:44 +0200 Subject: [PATCH] [dataclasses] Fix MISSING and KW_ONLY fro Python 3.15+ --- stdlib/@tests/stubtest_allowlists/common.txt | 2 - stdlib/@tests/stubtest_allowlists/py310.txt | 2 + stdlib/@tests/stubtest_allowlists/py311.txt | 2 + stdlib/@tests/stubtest_allowlists/py312.txt | 2 + stdlib/@tests/stubtest_allowlists/py313.txt | 2 + stdlib/@tests/stubtest_allowlists/py314.txt | 2 + stdlib/@tests/stubtest_allowlists/py315.txt | 3 - stdlib/dataclasses.pyi | 83 ++++++++++++++++---- 8 files changed, 79 insertions(+), 19 deletions(-) diff --git a/stdlib/@tests/stubtest_allowlists/common.txt b/stdlib/@tests/stubtest_allowlists/common.txt index 29d91719c50c..7a51c78265a0 100644 --- a/stdlib/@tests/stubtest_allowlists/common.txt +++ b/stdlib/@tests/stubtest_allowlists/common.txt @@ -242,8 +242,6 @@ _?ctypes.Array.__iter__ calendar._localized_day.__iter__ calendar._localized_month.__iter__ -dataclasses.KW_ONLY # white lies around defaults - # __all__-related weirdness (see #6523) email.__all__ email.base64mime diff --git a/stdlib/@tests/stubtest_allowlists/py310.txt b/stdlib/@tests/stubtest_allowlists/py310.txt index 4f1fe3bae1fd..eba112e199ae 100644 --- a/stdlib/@tests/stubtest_allowlists/py310.txt +++ b/stdlib/@tests/stubtest_allowlists/py310.txt @@ -2,6 +2,8 @@ # New errors in Python 3.10 # ========================= +dataclasses.KW_ONLY # white lies around defaults + # ========= # 3.10 only # ========= diff --git a/stdlib/@tests/stubtest_allowlists/py311.txt b/stdlib/@tests/stubtest_allowlists/py311.txt index 69f314e576ab..cc44ef9aa7bd 100644 --- a/stdlib/@tests/stubtest_allowlists/py311.txt +++ b/stdlib/@tests/stubtest_allowlists/py311.txt @@ -2,6 +2,8 @@ # New errors in Python 3.11 # ========================= +dataclasses.KW_ONLY # white lies around defaults + # ======= # >= 3.11 # ======= diff --git a/stdlib/@tests/stubtest_allowlists/py312.txt b/stdlib/@tests/stubtest_allowlists/py312.txt index 28f037fe9099..8040ff8197b3 100644 --- a/stdlib/@tests/stubtest_allowlists/py312.txt +++ b/stdlib/@tests/stubtest_allowlists/py312.txt @@ -2,6 +2,8 @@ # New errors in Python 3.12 # ========================= +dataclasses.KW_ONLY # white lies around defaults + # ======= # >= 3.12 # ======= diff --git a/stdlib/@tests/stubtest_allowlists/py313.txt b/stdlib/@tests/stubtest_allowlists/py313.txt index 22dcac6f46ca..79d1d80ad58f 100644 --- a/stdlib/@tests/stubtest_allowlists/py313.txt +++ b/stdlib/@tests/stubtest_allowlists/py313.txt @@ -2,6 +2,8 @@ # New errors in Python 3.13 # ========================= +dataclasses.KW_ONLY # white lies around defaults + # ==================================== # Pre-existing errors from Python 3.12 # ==================================== diff --git a/stdlib/@tests/stubtest_allowlists/py314.txt b/stdlib/@tests/stubtest_allowlists/py314.txt index b76fa29cd7d2..927258a37db4 100644 --- a/stdlib/@tests/stubtest_allowlists/py314.txt +++ b/stdlib/@tests/stubtest_allowlists/py314.txt @@ -2,6 +2,8 @@ # New errors in Python 3.14 # ========================= +dataclasses.KW_ONLY # white lies around defaults + # Union and UnionType are aliases in 3.14 but type checkers need some changes typing.Union types.UnionType.__class_getitem__ diff --git a/stdlib/@tests/stubtest_allowlists/py315.txt b/stdlib/@tests/stubtest_allowlists/py315.txt index 7354dac4dc90..217b744f92c5 100644 --- a/stdlib/@tests/stubtest_allowlists/py315.txt +++ b/stdlib/@tests/stubtest_allowlists/py315.txt @@ -18,9 +18,6 @@ _struct.pack_into _thread.RLock.__exit__ _thread.lock.__exit__ copy.deepcopy -dataclasses.MISSING -dataclasses._MISSING_TYPE -dataclasses.field doctest.DocTestRunner.report_skip importlib._abc.Loader.load_module importlib._bootstrap_external.NamespacePath diff --git a/stdlib/dataclasses.pyi b/stdlib/dataclasses.pyi index fd5588e03490..d7f1eff5eca3 100644 --- a/stdlib/dataclasses.pyi +++ b/stdlib/dataclasses.pyi @@ -8,6 +8,9 @@ from types import GenericAlias from typing import Any, Final, Generic, Literal, Protocol, TypeVar, overload, type_check_only from typing_extensions import Never, TypeIs +if sys.version_info >= (3, 15): + from builtins import sentinel + _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -48,17 +51,23 @@ class _DataclassFactory(Protocol): weakref_slot: bool = False, ) -> type[_T]: ... -# define _MISSING_TYPE as an enum within the type stubs, -# even though that is not really its type at runtime -# this allows us to use Literal[_MISSING_TYPE.MISSING] -# for background, see: -# https://github.com/python/typeshed/pull/5900#issuecomment-895513797 -class _MISSING_TYPE(enum.Enum): - MISSING = enum.auto() +if sys.version_info >= (3, 15): + MISSING: Final[sentinel] +else: + # define _MISSING_TYPE as an enum within the type stubs, + # even though that is not really its type at runtime + # this allows us to use Literal[_MISSING_TYPE.MISSING] + # for background, see: + # https://github.com/python/typeshed/pull/5900#issuecomment-895513797 + class _MISSING_TYPE(enum.Enum): + MISSING = enum.auto() -MISSING: Final = _MISSING_TYPE.MISSING + MISSING: Final = _MISSING_TYPE.MISSING -class KW_ONLY: ... +if sys.version_info >= (3, 15): + KW_ONLY: Final[sentinel] +else: + class KW_ONLY: ... @overload def asdict(obj: DataclassInstance) -> dict[str, Any]: ... @@ -172,8 +181,16 @@ class Field(Generic[_T]): ) name: str type: Type[_T] | str | Any - default: _T | Literal[_MISSING_TYPE.MISSING] - default_factory: _DefaultFactory[_T] | Literal[_MISSING_TYPE.MISSING] + + if sys.version_info >= (3, 15): + default: _T | sentinel + default_factory: _DefaultFactory[_T] | sentinel + kw_only: bool | sentinel + else: + default: _T | Literal[_MISSING_TYPE.MISSING] + default_factory: _DefaultFactory[_T] | Literal[_MISSING_TYPE.MISSING] + kw_only: bool | Literal[_MISSING_TYPE.MISSING] + repr: bool hash: bool | None init: bool @@ -183,8 +200,6 @@ class Field(Generic[_T]): if sys.version_info >= (3, 14): doc: str | None - kw_only: bool | Literal[_MISSING_TYPE.MISSING] - if sys.version_info >= (3, 14): def __init__( self, @@ -216,7 +231,47 @@ class Field(Generic[_T]): # NOTE: Actual return type is 'Field[_T]', but we want to help type checkers # to understand the magic that happens at runtime. -if sys.version_info >= (3, 14): +if sys.version_info >= (3, 15): + @overload # `default` and `default_factory` are optional and mutually exclusive. + def field( + *, + default: _T, + default_factory: sentinel = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | sentinel = ..., + doc: str | None = None, + ) -> _T: ... + @overload + def field( + *, + default: sentinel = ..., + default_factory: Callable[[], _T], + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | sentinel = ..., + doc: str | None = None, + ) -> _T: ... + @overload + def field( + *, + default: sentinel = ..., + default_factory: sentinel = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | sentinel = ..., + doc: str | None = None, + ) -> Any: ... +elif sys.version_info >= (3, 14): @overload # `default` and `default_factory` are optional and mutually exclusive. def field( *,