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
11 changes: 8 additions & 3 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,14 @@ Module-level decorators, classes, and functions

def __init__(self, a: int, b: int = 0):

:exc:`TypeError` will be raised if a field without a default value
follows a field with a default value. This is true either when this
occurs in a single class, or as a result of class inheritance.
When a field without a default value follows a field with a default value
(including as a result of class inheritance), the former field, and all
successive fields, will be made keyword-only in the parameters of
:meth:`__init__`.

.. versionchanged:: 3.9
Non-defaulted fields following defaulted field used raise an
:exc:`TypeError`, but are now converted to keyword-only.

.. function:: field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)

Expand Down
31 changes: 15 additions & 16 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,21 +486,6 @@ def _init_param(f):
def _init_fn(fields, frozen, has_post_init, self_name, globals):
# fields contains both real fields and InitVar pseudo-fields.

# Make sure we don't have fields without defaults following fields
# with defaults. This actually would be caught when exec-ing the
# function source code, but catching it here gives a better error
# message, and future-proofs us in case we build up the function
# using ast.
seen_default = False
for f in fields:
# Only consider fields in the __init__ call.
if f.init:
if not (f.default is MISSING and f.default_factory is MISSING):
seen_default = True
elif seen_default:
raise TypeError(f'non-default argument {f.name!r} '
'follows default argument')

locals = {f'_type_{f.name}': f.type for f in fields}
locals.update({
'MISSING': MISSING,
Expand All @@ -525,8 +510,22 @@ def _init_fn(fields, frozen, has_post_init, self_name, globals):
if not body_lines:
body_lines = ['pass']

# Build arguments: once we've encountered defaulted fields, all
# following non-defaulted fields must be passed via keyword
seen_default = False
keyword_only = False
args = [self_name]
for f in fields:
if f.init:
if not (f.default is MISSING and f.default_factory is MISSING):
seen_default = True
elif seen_default and not keyword_only:
keyword_only = True
args.append("*")
args.append(_init_param(f))

return _create_fn('__init__',
[self_name] + [_init_param(f) for f in fields if f.init],
args,
body_lines,
locals=locals,
globals=globals,
Expand Down
103 changes: 80 additions & 23 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,40 +62,97 @@ class C:
o = C(3)
self.assertEqual((o.x, o.y), (3, 0))

def test_two_fields_one_default_nondefault_follows_default(self):
# Non-defaults following defaults.
@dataclass
class C:
x: int = 0
y: int

with self.assertRaisesRegex(TypeError,
"non-default argument 'y' follows "
"default argument"):
@dataclass
class C:
x: int = 0
y: int
"__init__\\(\\) missing 1 "
"required keyword-only argument: "
"'y'"):
o = C(3)

with self.assertRaisesRegex(TypeError,
"__init__\\(\\) takes from 1 to "
"2 positional arguments but 3 "
"were given"):
o = C(3, 4)

o = C(3, y=4)
self.assertEqual((o.x, o.y), (3, 4))

o = C(x=2, y=4)
self.assertEqual((o.x, o.y), (2, 4))

o = C(y=5)
self.assertEqual((o.x, o.y), (0, 5))

def test_two_fields_one_default_derived_adds_nondefault(self):
# A derived class adds a non-default field after a default one.
@dataclass
class B:
x: int = 0

@dataclass
class C(B):
y: int

with self.assertRaisesRegex(TypeError,
"non-default argument 'y' follows "
"default argument"):
@dataclass
class B:
x: int = 0
"__init__\\(\\) missing 1 "
"required keyword-only argument: "
"'y'"):
o = C(3)

@dataclass
class C(B):
y: int
with self.assertRaisesRegex(TypeError,
"__init__\\(\\) takes from 1 to "
"2 positional arguments but 3 "
"were given"):
o = C(3, 4)

o = C(3, y=4)
self.assertEqual((o.x, o.y), (3, 4))

o = C(x=2, y=4)
self.assertEqual((o.x, o.y), (2, 4))

o = C(y=5)
self.assertEqual((o.x, o.y), (0, 5))

def test_two_fields_one_default_derived_updates_to_default(self):
# Override a base class field and add a default to
# a field which didn't use to have a default.
@dataclass
class B:
x: int
y: int

@dataclass
class C(B):
x: int = 0

with self.assertRaisesRegex(TypeError,
"non-default argument 'y' follows "
"default argument"):
@dataclass
class B:
x: int
y: int
"__init__\\(\\) missing 1 "
"required keyword-only argument: "
"'y'"):
o = C(3)

@dataclass
class C(B):
x: int = 0
with self.assertRaisesRegex(TypeError,
"__init__\\(\\) takes from 1 to "
"2 positional arguments but 3 "
"were given"):
o = C(3, 4)

o = C(3, y=4)
self.assertEqual((o.x, o.y), (3, 4))

o = C(x=2, y=4)
self.assertEqual((o.x, o.y), (2, 4))

o = C(y=5)
self.assertEqual((o.x, o.y), (0, 5))

def test_overwrite_hash(self):
# Test that declaring this class isn't an error. It should
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ Grant Olson
Koray Oner
Piet van Oostrum
Tomas Oppelstrup
Laurie Opperman
Jason Orendorff
Bastien Orivel
orlnub123
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When a non-defaulted dataclass field follows a defaulted field, the behaviour has changed from raising a ``TypeError`` to making all arguments in ``__init__`` following the defaulted field keyword-only.