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
- neither is required during initialization because
- all of them will receive
None
as their value, when no other value is provided,
- 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
).