pydantic icon indicating copy to clipboard operation
pydantic copied to clipboard

Type signature for Field() gt/lt/gte/lte validators should include timedelta

Open bjmc opened this issue 1 year ago • 6 comments

Initial Checks

  • [X] I confirm that I'm using Pydantic V2

Description

The type signature for the numeric min/max value constraints on Field() (i.e. gt, lt, gte, lte ) specified here is too restrictive, allowing only float or None.

In actual use, the validation works correctly as expected with timedelta() instances, but static type checkers (at least Pyright) object.

Example Code

from datetime import timedelta

from pydantic import BaseModel, Field

class Duration(BaseModel):
    td: timedelta = Field(timedelta(0), gt=timedelta(0))


Duration(td=timedelta(seconds=1))
Duration(td=timedelta(seconds=-1))
# ValidationError: 1 validation error for Duration
# td
#  Input should be greater than 0 seconds [type=greater_than, input_value=datetime.timedelta(days=-1), input_type=timedelta]
#    For further information visit https://errors.pydantic.dev/2.7/v/greater_than

# This is all perfectly valid code, but Pyright (and I assume also mypy) complains about the type signature

# $ pyright example.py 
# /home/bjmc/Sandbox/example.py
#  /home/bjmc/Sandbox/example.py:6:44 - error: Argument of type "timedelta" cannot be assigned to # parameter "gt" of type "float | None" in function "Field"
#    Type "timedelta" is incompatible with type "float | None"
#      "timedelta" is incompatible with "float"
#      "timedelta" is incompatible with "None" (reportArgumentType)
# 1 error, 0 warnings, 0 informations

Python, Pydantic & OS Version

pydantic version: 2.7.1
        pydantic-core version: 2.18.2
          pydantic-core build: profile=release pgo=true
                 install path: /home/bjmc/.cache/pypoetry/virtualenvs/ska-oso-pdm-zPbyUnCb-py3.10/lib/python3.10/site-packages/pydantic
               python version: 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
                     platform: Linux-6.5.0-1022-oem-x86_64-with-glibc2.35
             related packages: typing_extensions-4.11.0 pyright-1.1.363
                       commit: unknown

bjmc avatar May 21 '24 15:05 bjmc

I'm not a typing wizard, but I wonder if a good resolution here would be to use the comparison protocols defined by typeshed for these field validators?

I'm happy to open an PR if people like that approach, or have another suggestion.

bjmc avatar May 21 '24 15:05 bjmc

@bjmc,

Hmm, I'm intrigued - feel free to open a PR and I can review. I don think it could be good to offer more general support here!

sydney-runkle avatar May 21 '24 15:05 sydney-runkle

Seems reasonable to use the comparison protocols, internally Pydantic uses the functions defined in pydantic._internal._validators (e.g. greater_than_validator), so this seems like a safe improvement.

Two options here:

  • Use the protocols defined in annotated_types (best option imo).
  • Import the protocols from _typeshed in an if TYPE_CHECKING: block. They are not defined as being stable.

Viicos avatar May 21 '24 18:05 Viicos

Let's go with the annotated_types protocols

sydney-runkle avatar May 22 '24 13:05 sydney-runkle

I agree that annotated_types is better since it's already a dependency.

bjmc avatar May 22 '24 14:05 bjmc

@sydney-runkle I've got a draft PR up. I'm not quite sure where to add a test for this, since it's only a type-checking issue. Suggestions?

bjmc avatar May 22 '24 16:05 bjmc

@bjmc,

Perhaps you could just add a before / after example to this PR running pyright on a file that uses a timedelta gt constraint as a proof of concept? PR looks great to me!

Update: I've added some notes to your PR re testing + some change requests!

sydney-runkle avatar May 28 '24 19:05 sydney-runkle