Pydantic inconsistent and automatic conversion between float and int

Question:

I am using pydantic python package in FastAPI for a web app, and I noticed there is some inconsistent float-int conversions with different typing checks.
For example:

class model(BaseModel):
    data: Optional[Union[int, float]] = None
m = model(data=3.33)
m.data --> 3.33

class model(BaseModel):
    data: Optional[Union[int, float, str]] = None
m = model(data=3.33)
m.data --> 3

class model(BaseModel):
    data: Union[int, float, str] = None
m = model(data=3.33)
m.data --> 3

class model(BaseModel):
    data: Union[str, int, float] = None
m = model(data=3.33)
m.data --> '3.33'

As shown here, different orders/combinations of typings have different behaviors.

I checked out thread https://github.com/samuelcolvin/pydantic/issues/360, and https://github.com/samuelcolvin/pydantic/issues/284, but they seem not to be the exact same problem.

What causes such behavior under the hood? Is there a specific reason for this? Or did I do anything wrong/inappropriate here?

I’m using python 3.8, pydantic 1.8.2

Thank you for helping!

—— Update ——

In pydantic==1.9.1 this seems has been fixed -> refer to @JacekK’s answer.

Asked By: Sammy Cui

||

Answers:

To understand what happened there we have to know how the Union works and how pydantic uses typing to validate values.

Union

According to the documentation:

Union type; Union[X, Y] is equivalent to X | Y and means either X or Y.

OR means that at least one element has to be true to make the whole sentence true. So, if the first element is true there’s no need to check the second element. Because the whole sentence is true no matter the value of the second element. Classical disjunction

Pydantic

Pydantic gets the given value and tries to map it to the type declared in the class attribute definition. If it succeeds then pydantic assigns value to the field. Otherwise it raises a ValidationError.

pseudo code:

x.data: int
x.data = "3" # string
x.data = int("3") #convertion,
x.data → 3

how it works in your case:

x.data = Union[int, float, str]
x.data = 3.33
x.data = int(3.33) #convertion to int, which is first in your Union
# because previous was success, then:
x.data → 3

This behavior is known and well documented:

As such, it is recommended that when defining Union annotations, the most specific type is included first and followed by less specific types.

class MyModel(BaseModel):
    data: Union[float, int, str]


m = MyModel(data=3.33)
print(m.data)
# output is: 3.33

m = MyModel(data="3.33")
print(m.data)
# output is: 3.33

m = MyModel(data=3)
print(m.data)
# output is: 3.0
Answered By: JacekK
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.