16

In Python 3.7, I can create a dataclass with a defaulted InitVar just fine:

from dataclasses import dataclass, InitVar, field

@dataclass
class Foo:
    seed: InitVar[str] = field(default='tomato')
    stored: str = field(init=False)

    def __post_init__(self, seed: str):
        self.stored = f'planted {seed}'

print(Foo())

Now I try to create a similar dataclass with a mutable default, for which I need to use default_factory instead:

from dataclasses import dataclass, InitVar, field
from typing import List

@dataclass
class Bar:
    seeds: InitVar[List[str]] = field(default_factory=list)
    stored: List[str] = field(init=False)

    def __post_init__(self, seeds: List[str]):
        self.stored = [f'planted {seed}' for seed in seeds]

print(Bar())

However, this is not valid. Python raises TypeError: field seeds cannot have a default factory.

The dataclasses.py file from the standard library does not explain why:

    # Special restrictions for ClassVar and InitVar.
    if f._field_type in (_FIELD_CLASSVAR, _FIELD_INITVAR):
        if f.default_factory is not MISSING:
            raise TypeError(f'field {f.name} cannot have a '
                            'default factory')
        # Should I check for other field settings? default_factory
        # seems the most serious to check for.  Maybe add others.  For
        # example, how about init=False (or really,
        # init=<not-the-default-init-value>)?  It makes no sense for
        # ClassVar and InitVar to specify init=<anything>.

Why? What is the rationale behind this special restriction? How does this make sense?

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
Jacobo de Vera
  • 1,863
  • 1
  • 16
  • 20
  • 2
    I would expect because you can write code in `__post_init__` to handle this case, with the idiomatic Python `if foo is None: foo = []`. Non-post-init values don’t have that luxury and *need* a factory. Having both a factory and custom post-init processing may be deemed potentially confusing…? – deceze Jul 16 '19 at 11:44
  • I wonder if there's some corner case where the factory could be invoked in `__post_init__` without knowing that it was already invoked in `__init__`. This code only exists in the commit that introduced `dataclasses.py`, so no help from the Git log. Your best chance at a definitive answer might be to ask on one of the mailing lists, where the original author could provide some insight. (Not sure which mailing list would be most appropriate: `python-dev` or `python-help`, perhaps.) – chepner Jul 16 '19 at 14:24
  • This one really has me stumped. The comments in code and tests all imply that it is obvious why InitVar default factories make no sense, but I just don't see why. – Arne Jul 16 '19 at 14:56
  • I have asked in python-help now: https://groups.google.com/forum/#!topic/comp.lang.python/hskZcjZfm40 – Jacobo de Vera Jul 17 '19 at 10:50
  • 4
    The author does not remember, but I will continue investigating. https://github.com/ericvsmith/dataclasses/issues/147 – Jacobo de Vera Jul 20 '19 at 09:18

1 Answers1

8

The rationale is that supplying a default_factory would almost always be an error.

The intent of InitVar is to create a pseudo-field, called an "init-only field". That is almost always populated by post_init() if the value is other than the default. It is never returned by module-level fields() function. The primary use case is initializing field values that depend on one or more other fields.

Given this intent, it would almost always be a user error to supply a default_factory which is:

  1. Something we would want to see returned by the fields() function.

  2. Entirely unnecessary if we're using post_init() where you can call a factory directly.

  3. Not suited for the case where the object creation depends on other field values.

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • Just had a "duh!" moment after all this time brought to me by this sentence: "Entirely unnecessary if we're using post_init() where you can call a factory directly." thank you, @raymond-hettinger – Jacobo de Vera Sep 10 '22 at 15:18