18

I am trying to validate an object that has "optional" fields in the sense that they may or may not be present. But when they are present, the fields should conform to a specific type definition (not None).

In the example below, the "size" field is optional but allows None. I want the "size" field to be optional, but if present it should be a float.

from pydantic import BaseModel

class Foo(BaseModel):
    count: int
    size: float = None  # how to make this an optional float? 

 >>> Foo(count=5)
 Foo(count=5, size=None)  # GOOD - "size" is not present, value of None is OK


 >>> Foo(count=5, size=None)
 Foo(count=5, size=None) # BAD - if field "size" is present, it should be a float

 # BONUS
 >>> Foo(count=5)
 Foo(count=5)  # BEST - "size" is not present, it is not required to be present, so we don't care about about validating it all.  We are using Foo.json(exclude_unset=True) handles this for us which is fine.
mgcdanny
  • 1,082
  • 1
  • 13
  • 20
  • This is how it should be done with consistent semantics. Anything "optional" doesn't have to be provided. An "optional" field is one that isn't necessarily present. Therefore an "optional" field with no default (no `None` default) that is provided must conform to it's type. That's "normal" thinking. It's just that Python has had this typing characteristic for so long now where an "optional" field is not in fact optional, it is mandatory, but it just has a `None` default that's injected if the field is not provided. I guess Python is now on the journey to fix that historical artifact. – NeilG Sep 03 '23 at 04:57
  • I'm not sure Pydantic 2 has a way to specify a genuinely optional field yet. I can't see a way to specify an optional field without a default. It doesn't matter whether the field allows `None` or is a strict not `None` monotype, you can't specify a field that can be allowed to be missing altogether: https://docs.pydantic.dev/latest/migration/#required-optional-and-nullable-fields – NeilG Sep 03 '23 at 05:05

3 Answers3

8

It's possible to do with a validator.

from pydantic import BaseModel, ValidationError, validator

class Foo(BaseModel):
    count: int
    size: float = None

    @validator('size')
    def size_is_some(cls, v):
        if v is None:
            raise ValidationError('Cannot set size to None')
        return float(v)

This works as intended:

>>> Foo(count=5)
Foo(count=5, size=None)

>>> Foo(count=5, size=1.6)
Foo(count=5, size=1.6)

>>> Foo(count=5, size=None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[venv]/lib/python3.6/site-packages/pydantic/main.py", line 283, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Foo
size
  Cannot set size to None (type=value_error)
Joe Moon
  • 104
  • 1
  • 10
Stig Johan B.
  • 371
  • 2
  • 5
7
from pydantic import BaseModel
from typing import Optional

class Foo(BaseModel):
    count: int
    size: Optional[float]


obj = Foo(count=5)
obj_2 = Foo(count=5, size=1.6)

# count=5 size=None
print(obj)
# count=5 size=1.6
print(obj_2)

It can be confusing but Optional type from typing will give you possibility to have "optional" fields

  • This answer has the same problem as . Mypy will require you to `assert obj.size is not None` (or equivalent) every time you use it. – shadowtalker Nov 05 '22 at 15:59
0
from pydantic import BaseModel, field_validator
from typing import Optional

class Foo(BaseModel):
   count: int
   size: Optional[float]= None

   field_validator("size")
   @classmethod
   def prevent_none(cls, v: float):
       assert v is not None, "size may not be None"
       return v
elyte5star
  • 167
  • 5