Python dataclass: can you set a default default for fields?

Viewed 9774

I want to make a dataclass base class where all the fields in subclasses are automatically Optional and default to None (if no default is provided).

The following code... almost seems to do what I want, but not quite. It errors out the same way as though I'd never written the __init_subclass__ (i.e. complains about unfilled parameters)... perhaps because my code is running after the dataclass magic happens?

@dataclass(order=True, frozen=True)
class BaseDictKey:
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        # noinspection PyUnresolvedReferences
        for field in cls.__dataclass_fields__.values():
            field.default = None if field.default is None else field.default
            field.type = typing.Union[field.type, NoneType]

@dataclass(order=True, frozen=True)
class ScoreDictKey(BaseDictKey):
    catalog: str  # magically becomes catalog: Optional[str] = None
    dataset: str = 'foo'  # magically becomes dataset: Optional[str] = 'foo'

(In case you're wondering why I want this, I have another baseclass that uses these BaseDictKeys that expects any and all fields in subclasses to be Optional. I suppose I could instead raise an exception if I detect something isn't an Optional, but that seems uglier.)

Is this possible in Python 3.7+?

1 Answers

I found a way with modifying class __annotations__ field to make fields optional and setting attributes directly on class to provide default value None:

from dataclasses import dataclass

import typing


@dataclass(order=True, frozen=True)
class BaseDictKey:
    def __init_subclass__(cls, *args, **kwargs):
        for field, value in cls.__annotations__.items():
            cls.__annotations__[field] = typing.Union[value, None]
            if not hasattr(cls, field):
                setattr(cls, field, None)
        super().__init_subclass__(*args, **kwargs)


@dataclass(order=True, frozen=True)
class ScoreDictKey(BaseDictKey):
    catalog: str  # magically becomes catalog: Optional[str] = None
    dataset: str = 'foo'  # magically becomes dataset: Optional[str] = 'foo'

    def __init__(self, *args, **kwargs):  # to get rid of PyCharm warning
        pass


c = ScoreDictKey()
print(c.catalog, c.dataset)  # None foo
Related