Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions stdlib/@tests/stubtest_allowlists/common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions stdlib/@tests/stubtest_allowlists/py310.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# New errors in Python 3.10
# =========================

dataclasses.KW_ONLY # white lies around defaults

# =========
# 3.10 only
# =========
Expand Down
2 changes: 2 additions & 0 deletions stdlib/@tests/stubtest_allowlists/py311.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# New errors in Python 3.11
# =========================

dataclasses.KW_ONLY # white lies around defaults

# =======
# >= 3.11
# =======
Expand Down
2 changes: 2 additions & 0 deletions stdlib/@tests/stubtest_allowlists/py312.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# New errors in Python 3.12
# =========================

dataclasses.KW_ONLY # white lies around defaults

# =======
# >= 3.12
# =======
Expand Down
2 changes: 2 additions & 0 deletions stdlib/@tests/stubtest_allowlists/py313.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# New errors in Python 3.13
# =========================

dataclasses.KW_ONLY # white lies around defaults

# ====================================
# Pre-existing errors from Python 3.12
# ====================================
Expand Down
2 changes: 2 additions & 0 deletions stdlib/@tests/stubtest_allowlists/py314.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
3 changes: 0 additions & 3 deletions stdlib/@tests/stubtest_allowlists/py315.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 69 additions & 14 deletions stdlib/dataclasses.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm this might regress functionality, can't we already do MISSING = sentinel("MISSING")?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @JelleZijlstra, that makes perfect sense. This was my first contribution, inspired by the EuroPython sprints, where the dataclasses stubs were listed as candidates for a fix.

It looks however like the correct fix is currently blocked by a tooling constraint: typeshed pins mypy to 2.3.0, which does not yet support sentinel values as types. That work is pending in python/mypy#21647.

Should we close this PR for now and revisit it once the mypy PR is merged and typeshed updates its pin, or would you prefer to keep it open as a draft until then?

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]: ...
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is worse than before, we should hint the specific sentinel not the sentinel type.

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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
*,
Expand Down