11

My type checker moans at me when I use snippets like this one from the Pydantic docs:

from datetime import datetime

from pydantic import BaseModel, validator


class DemoModel(BaseModel):
    ts: datetime = None  # Expression of type "None" cannot be 
                         # assigned to declared type "datetime"

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()

My workarounds so far have been:

ts: datetime = datetime(1970, 1, 1)  # yuck
ts: datetime = None  # type: ignore
ts: Optional[datetime] = None  # Not really true. `ts` is not optional.

Is there a preferred way out of this conundrum?

Or is there a type checker I could use which doesn't mind this?

LondonRob
  • 73,083
  • 37
  • 144
  • 201
  • @daniel-quinn's solution is correct, but I'd argue that `ts` _is_ optional - the validator will set `ts=datetime.now()` if it hasn't been supplied at initialisation. So your third option (`Optional[datetime]`) also works. – Ogaday Mar 17 '22 at 12:18
  • 1
    @Ogaday that's an interesting take on what `Optional` means, but it'll mean I have to do `if DemoModel.ts:` everywhere, which is an unnecessary check because it'll always have a value. – LondonRob Mar 17 '22 at 12:24

2 Answers2

16

New answer

Use a Field with a default_factory for your dynamic default value:

from datetime import datetime

from pydantic import BaseModel, Field


class DemoModel(BaseModel):
    ts: datetime = Field(default_factory=datetime.now)

Your type hints are correct, the linter is happy and DemoModel().ts is not None.

From the Field docs:

default_factory: a zero-argument callable that will be called when a default value is needed for this field. Among other purposes, this can be used to set dynamic default values.

Ogaday
  • 469
  • 3
  • 13
  • This (the new answer) is pretty good, and has led me to some useful reading about `Field`. I think where the default value can be usefully put into a lambda, then this is the right approach. It's got some downsides though, because the factory is zero-argument: no `cls` to use class methods, or `values` to derive the default from. – LondonRob Mar 17 '22 at 13:08
  • I don't know if you can combine `Field`s with validators. You can potentially look at [custom Fields](https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types) for more powerful behaviour, but it may be overkill. – Ogaday Mar 17 '22 at 13:15
  • Turns out you can combine `Field`s with validators and handle the dynamic default there instead of the `default_factory` – Ogaday Mar 17 '22 at 13:20
2

If the field is required, then you just don't supply a default:

class DemoModel(BaseModel):
    ts: datetime

Pydantic will prevent you from instantiating an instance of DemoModel if you don't supply a ts argument in this case.

Daniel Quinn
  • 1,095
  • 1
  • 6
  • 7
  • 1
    The whole point of `always=True` on the validator is "to set a dynamic default value." (qutote from [the docs](https://pydantic-docs.helpmanual.io/usage/validators/#validate-always)). So I want to be able to instantiate without providing a value. – LondonRob Mar 17 '22 at 12:23
  • Yeah my bad. I read the question and ignored the validator not realising that you were using the validator to set a default. Ogaday's answer above looks like the right way to go for your case. – Daniel Quinn Mar 17 '22 at 13:41