Skip to content
Closed
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
43 changes: 35 additions & 8 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ Module contents
additional information, you can replace the default field value
with a call to the provided :func:`field` function. For example::

@dataclass
class C:
mylist: Annotated[list[int], field(default_factory=list)]

c = C()
c.mylist += [1, 2, 3]

.. versionchanged:: 3.12

Prior to this, `field` had to be specified as the default value of the
class attribute, as in::

@dataclass
class C:
mylist: list[int] = field(default_factory=list)
Expand Down Expand Up @@ -292,7 +304,24 @@ Module contents

.. versionadded:: 3.10

If the default value of a field is specified by a call to
When using ``Annotated`` to provide a extra information for the field,
a ``default`` value can be specified as usual. For example::

@dataclass
class C:
x: int
y: Annotated[int, field(repr=False)]
z: Annotated[int, field(repr=False)] = 10
t: int = 20

.. versionchanged:: 3.12

The class attribute ``C.z`` will be ``10``, the class attribute
``C.t`` will be ``20``, and the class attributes ``C.x`` and
``C.y`` will not be set.

Ussing the previous way of assigning a :func:`field` to the attribute,
if the default value of a field is specified by a call to
:func:`field()`, then the class attribute for this field will be
replaced by the specified ``default`` value. If no ``default`` is
provided, then the class attribute will be deleted. The intent is
Expand All @@ -308,9 +337,7 @@ Module contents
z: int = field(repr=False, default=10)
t: int = 20

The class attribute ``C.z`` will be ``10``, the class attribute
``C.t`` will be ``20``, and the class attributes ``C.x`` and
``C.y`` will not be set.
The result being the same as in the ``Annotated`` version above

.. class:: Field

Expand Down Expand Up @@ -526,7 +553,7 @@ Post-init processing
class C:
a: float
b: float
c: float = field(init=False)
c: Annotated[float, field(init=False)]

def __post_init__(self):
self.c = self.a + self.b
Expand Down Expand Up @@ -663,7 +690,7 @@ fields, and ``Base.x`` and ``D.z`` are regular fields::
@dataclass
class D(Base):
z: int = 10
t: int = field(kw_only=True, default=0)
t: Annotated[int, field(kw_only=True] = 0

The generated :meth:`~object.__init__` method for ``D`` will look like::

Expand All @@ -684,7 +711,7 @@ If a :func:`field` specifies a ``default_factory``, it is called with
zero arguments when a default value for the field is needed. For
example, to create a new instance of a list, use::

mylist: list = field(default_factory=list)
mylist: Annotated[list, field(default_factory=list)]

If a field is excluded from :meth:`~object.__init__` (using ``init=False``)
and the field also specifies ``default_factory``, then the default
Expand Down Expand Up @@ -748,7 +775,7 @@ mutable types as default values for fields::

@dataclass
class D:
x: list = field(default_factory=list)
x: Annotated[list, field(default_factory=list)]

assert D().x is not D().x

Expand Down
77 changes: 68 additions & 9 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,13 @@ def _is_initvar(a_type, dataclasses):
return (a_type is dataclasses.InitVar
or type(a_type) is dataclasses.InitVar)


def _is_annotated(a_type, typing):
# This test uses a typing internal class, but it's the best way to
# test if this is Annotated.
return a_type is typing.Annotated or type(a_type) is typing._AnnotatedAlias


def _is_kw_only(a_type, dataclasses):
return a_type is dataclasses.KW_ONLY

Expand Down Expand Up @@ -763,16 +770,68 @@ def _get_field(cls, a_name, a_type, default_kw_only):
# default_kw_only is the value of kw_only to use if there isn't a field()
# that defines it.

typing = sys.modules.get('typing') # detect typing early for Annotated

# If the default value isn't derived from Field, then it's only a
# normal default value. Convert it to a Field().
default = getattr(cls, a_name, MISSING)
if isinstance(default, Field):
f = default
else:
if isinstance(default, types.MemberDescriptorType):
# This is a field in __slots__, so it has no default value.
default = MISSING
f = field(default=default)

if isinstance(default, types.MemberDescriptorType):
# This is a field in __slots__, so it has no default value.
default = MISSING

# Look early for Annotated, because it may host a `Field` definition,
# to be then used instead of creating a blank Field
f = None
# If the Field is in an Annotated typehint, it will be in ann_a_type. It
# will be uaed as a flag to skip the lassVar check later
ann_a_type = None

if typing:
if _is_annotated(a_type, typing):
ann_a_type, *ann_args = typing.get_args(a_type)
elif (isinstance(a_type, str)
and _is_type(a_type, cls, typing, typing.Annotated,
_is_annotated)):

# eval annotated string
br0, br1 = a_type.index("["), a_type.rindex("]")
eval_str = a_type[br0:br1].lstrip("[ ").rstrip(" ]")
# Annotated args "guaranteed" to be at least a 2-tuple. Unpacking
# can be done safely after eval. If the user has manually added an
# annotation in string format which does not match the minimum
# "Annotated" requirements an exception will be raised, which
# should be the expected behavior.
ann_a_type, *ann_args = eval(eval_str)

if ann_a_type is not None: # annotated detected - look for Field
for ann_arg in ann_args:
if isinstance(ann_arg, Field):
f = ann_arg # reuse the field
defmissing = default is MISSING
if f.default is not MISSING and not defmissing:
msg = ("If 'field' has a default value inside "
"'Annotated', the field cannot also be "
"assigned a default value")
raise ValueError(msg)

if f.default_factory is not MISSING and not defmissing:
msg = ("If 'field' has a default_factory inside"
"'Annotated', the field cannot also be "
"assigned a default value")
raise ValueError(msg)

if isinstance(default, Field):
msg = ("If 'field' is specified in 'Annotated' it "
"cannot be also assigned as default value")
raise ValueError(msg)

f.default = default # record fetched default
a_type = ann_a_type # record real type
break # only 1st Field considered

if f is None: # search for Annotated was not successful
f = default if isinstance(default, Field) else field(default=default)

# Only at this point do we know the name and the type. Set them.
f.name = a_name
Expand All @@ -797,8 +856,8 @@ def _get_field(cls, a_name, a_type, default_kw_only):
# annotation to be a ClassVar. So, only look for ClassVar if
# typing has been imported by any module (not necessarily cls's
# module).
typing = sys.modules.get('typing')
if typing:
# Do only do the check if the value was not "Annotated" with a Field
if ann_a_type is None and typing:
if (_is_classvar(a_type, typing)
or (isinstance(f.type, str)
and _is_type(f.type, cls, typing, typing.ClassVar,
Expand Down