4

What is the distinction between implicitly setting an optional attribute to None with typing.Optional[] versus explicitly assigning typing.Optional[] = None when creating Pydantic models? In both cases, the attribute will eventually have a value of None when the class object is instantiated.

import typing
import pydantic


class Bar(pydantic.BaseModel):
    a: typing.Optional[int]

    @pydantic.validator('a', always=True, pre=True)
    def check_a(cls, v, values, field):
        print("WITHOUT NONE")
        print(values)
        print(field)
        return v


class Baz(pydantic.BaseModel):
    a: typing.Optional[int] = None

    @pydantic.validator('a', always=True, pre=True)
    def check_a(cls, v, values, field):
        print("WITH NONE")
        print(values)
        print(field)
        return v


print(Bar())
print(Baz())

Output:

WITHOUT NONE
{}
name='a' type=Optional[int] required=False default=None
a=None
WITH NONE
{}
name='a' type=Optional[int] required=False default=None
a=None
Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
  • If you don't give it an initial value, you'll get an error if you try to use the variable before assigning it. – Barmar Jun 13 '23 at 15:38
  • @Barmar This is the case with _all other_ types. But not with unions with `None`. It is a deliberate inconsistency in Pydantic. `Optional[T]` fields implicitly always have the `None` default value (unless another is specified. – Daniil Fajnberg Jun 13 '23 at 15:54

2 Answers2

2

TL;DR

There is no difference. At least in terms of functionality. One implicitly sets the default value as None (unless another default is specified). The other explicitly sets the default as None.


Details

A deliberate (?) inconsistency

You may already know that Optional[T] is equivalent to Union[T, None] or T | None (in the newer notation). In all other cases with types T and U simply annotating a field as a: T | U and not providing an explicit default value would make that a required field. In the Pydantic lingo that means you cannot instantiate the model without (somehow) providing a value for that field and that value will have to be either of type T or of type U to pass validation.

The union T | None is an exception to this rule that is (arguably) not obvious, so you might call it an inconsistency. But it seems to be intentional. Annotating a field with such a union (so for example a: Optional[T]) will always create a field that is not required and has the default value None (unless of course you specify some other default).

Implementation details

Under the hood, this happens during model creation after a ModelField instance has been all but created and its prepare method is called. It calls the _type_analysis method to determine more details about the field type. And after recognizing the type origin to be a union, its type arguments are looked at in turn and once one of them is determined to be the NoneType, the field's required attribute is set to False and its allow_none attribute to True. Then, a bit later because the field still has an undefined default attribute, that attribute is set to None. That last step is obviously skipped, if you explicitly define a default value (None or some instance of T) or a default factory.

Runtime implications

In practice this means that for the following model all fields behave identically in the sense that

  1. neither is required during initialization because
  2. all of them will receive None as their value, when no other value is provided,
  3. and the type of each of them is set to be Optional[str].
from typing import Optional, Union
from pydantic import BaseModel, Field


class Model(BaseModel):
    a: str | None
    b: Union[str, None]
    c: Optional[str]
    d: Optional[str] = None
    e: Optional[str] = Field(default=None)
    f: Optional[str] = Field(default_factory=lambda: None)
    g: str = None


obj = Model()
print(obj.json(indent=4))

Output:

{
    "a": null,
    "b": null,
    "c": null,
    "d": null,
    "e": null,
    "f": null,
    "g": null
}

Note that even g is implicitly handled in a way that sets its type to be Optional[str], even though we annotate it with just str. You can verify this by doing the following:

for field in Model.__fields__.values():
    print(repr(field))

Output:

ModelField(name='a', type=Optional[str], required=False, default=None)
ModelField(name='b', type=Optional[str], required=False, default=None)
ModelField(name='c', type=Optional[str], required=False, default=None)
ModelField(name='d', type=Optional[str], required=False, default=None)
ModelField(name='e', type=Optional[str], required=False, default=None)
ModelField(name='f', type=Optional[str], required=False, default_factory='<function <lambda>>')
ModelField(name='g', type=Optional[str], required=False, default=None)

By the way, you can also make a field of the type Optional[T] and required. To do that you simply have to set the default to the ellipsis ..., so field: Optional[str] = ... for example would make that field accept None as a value, but still require you to provide a value during initialization. (see docs)

Clean code considerations

This is more subjective of course, but I would suggest adhering to the Zen of Python:

Explicit is better than implicit.

Especially since this behavior is so inconsistent and not obvious unless you are already familiar with Pydantic, you should not omit the default value even though you can. It just takes you a few more characters to add = None to the field definition, but it is much clearer for you later on or some other person reading the code.

I would also not recommend omitting the NoneType from the annotation. The reason is the same. Field g from the previous example might work just fine, but it undergoes an implicit change of its type.

My recommendation is to do field: Optional[str] = None, if you are dealing with just one additional type and field: str | int | None, if there are more types (or the older Union notation, if you are on Python <=3.9).

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
2

Whilst the previous answer is correct for pydantic v1, note that pydantic v2, released 2023-06-30, changed this behavior.

As specified in the migration guide:

Pydantic V2 changes some of the logic for specifying whether a field annotated as Optional is required (i.e., has no default value) or not (i.e., has a default value of None or any other value of the corresponding type), and now more closely matches the behavior of dataclasses. Similarly, fields annotated as Any no longer have a default value of None.

elachere
  • 595
  • 2
  • 8
  • 20