How to create an optional field in a dataclass that is inherited?

sparkstar picture sparkstar · Apr 30, 2020 · Viewed 11.6k times · Source
from typing import Optional

@dataclass
class Event:
   id: str
   created_at: datetime
   updated_at: Optional[datetime]
   #updated_at: datetime = field(default_factory=datetime.now)  CASE 1
   #updated_at: Optional[datetime] = None                       CASE 2

@dataclass
class NamedEvent(Event):
  name: str

When creating an event instance I will generally not have an updated_at field. I can either pass the current time as default value or add a value to it when making the insertion in the database and fetch it in subsequent uses of the object. Which is the better way? As per my understanding, I cannot create a NamedEvent instance without passing the updated_at field in case1 and case2 since I do not have a default value in name field.

Answer

Arne picture Arne · May 2, 2020

The underlying problem that you have seems to be the same one that is described here. The short version of that post is that in a function signature (including the dataclass-generated __init__ method), obligatory arguments (like NamedEvent's name) can not follow after arguments with default values (which are necessary to define the behavior of Event's updated_at) - a child's fields will always follow after those of its parent.

So either you have no default values in your parent class (which doesn't work in this case) or all your child's fields need default values (which is annoying, and sometimes simply not feasible).

The post I linked above discusses some patterns that you can apply to solve your problem, but as a nicer alternative you can also use the third part party package pydantic which already solved this problem for you. A sample implementation could look like this:

import pydantic
from datetime import datetime


class Event(pydantic.BaseModel):
    id: str
    created_at: datetime = None
    updated_at: datetime = None

    @pydantic.validator('created_at', pre=True, always=True)
    def default_created(cls, v):
        return v or datetime.now()

    @pydantic.validator('updated_at', pre=True, always=True)
    def default_modified(cls, v, values):
        return v or values['created_at']


class NamedEvent(Event):
    name: str

The default-value specification through validators is a bit cumbersome, but overall it's a very useful package that fixes lots of the shortcomings that you run into when using dataclasses, plus some more.

Using the class definition, an instance of NamedEvent can be created like this:

>>> NamedEvent(id='1', name='foo')
NamedEvent(id='1', created_at=datetime.datetime(2020, 5, 2, 18, 50, 12, 902732), updated_at=datetime.datetime(2020, 5, 2, 18, 50, 12, 902732), name='foo')