Is there a way to specify a range of valid values for a function argument with type hinting in python?
Question:
I am a big fan of the type hinting in python, however I am curious if there is a way to specify a valid range of values for a given parameter using type hinting.
What I had in mind is something like
from typing import *
def function(
number: Union[float, int],
fraction: Float[0.0, 1.0] = 0.5 # give a hint that this should be between 0 and 1,
):
return fraction * number
I can imagine one can enforce this with an assertion, or perhaps specify what the valid range of values is within the docstring, but it feels like having something like Float[0.0, 1.0] would look more elegant.
Answers:
Python 3.9 introduced typing.Annotated
:
In [75]: from typing import *
In [76]: from dataclasses import dataclass
In [77]: @dataclass
...: class ValueRange:
...: min: float
...: max: float
...:
In [78]: def function(
...: number: Union[float, int],
...: fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
...: ):
...: return fraction * number
...:
Like any other type hint it does not perform any runtime checks:
In [79]: function(1, 2)
Out[79]: 2
However you can implement your own runtime checks. The code below is just an example, it does not cover all cases and probably an overkill for your simple function:
In [88]: import inspect
In [89]: @dataclass
...: class ValueRange:
...: min: float
...: max: float
...:
...: def validate_value(self, x):
...: if not (self.min <= x <= self.max):
...: raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
...:
In [90]: def check_annotated(func):
...: hints = get_type_hints(func, include_extras=True)
...: spec = inspect.getfullargspec(func)
...:
...: def wrapper(*args, **kwargs):
...: for idx, arg_name in enumerate(spec[0]):
...: hint = hints.get(arg_name)
...: validators = getattr(hint, '__metadata__', None)
...: if not validators:
...: continue
...: for validator in validators:
...: validator.validate_value(args[idx])
...:
...: return func(*args, **kwargs)
...: return wrapper
...:
...:
In [91]: @check_annotated
...: def function_2(
...: number: Union[float, int],
...: fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
...: ):
...: return fraction * number
...:
...:
In [92]: function_2(1, 2)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-92-c9345023c025> in <module>
----> 1 function_2(1, 2)
<ipython-input-90-01115cb628ba> in wrapper(*args, **kwargs)
10 continue
11 for validator in validators:
---> 12 validator.validate_value(args[idx])
13
14 return func(*args, **kwargs)
<ipython-input-87-7f4ac07379f9> in validate_value(self, x)
6 def validate_value(self, x):
7 if not (self.min <= x <= self.max):
----> 8 raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
9
ValueError: 2 must be in range [0.0, 1.0]
In [93]: function_2(1, 1)
Out[93]: 1
If you can and you don’t mind using third-party packages, Pydantic provides Constrained Types. For your specific example, one of the constrained types is confloat
with the following parameters:
ge: float = None
: enforces float to be greater than or equal to the set value
lt: float = None
: enforces float to be less than the set value
In [35]: from pydantic import confloat
In [36]: def function(
...: number: Union[float, int],
...: fraction: confloat(ge=0.0, le=1.0) = 0.5,
...: ) -> float:
...: return fraction * number
If only used as a type hint, it doesn’t enforce it at runtime:
In [38]: function(1, 0)
Out[38]: 0
In [39]: function(1, 1.0)
Out[39]: 1.0
In [40]: function(1, 15)
Out[40]: 15
But you can use Pydantic’s validate_arguments
decorator which:
allows the arguments passed to a function to be parsed and validated using the function’s annotations before the function is called
In [41]: from pydantic import confloat, validate_arguments
In [42]: @validate_arguments
...: def function(
...: number: Union[float, int],
...: fraction: confloat(ge=0.0, le=1.0) = 0.5,
...: ) -> float:
...: return fraction * number
...:
In [43]: function(1, 0)
Out[43]: 0.0
In [44]: function(1, 1.0)
Out[44]: 1.0
In [45]: function(1, 0.37)
Out[45]: 0.37
In [46]: function(1, 15)
---------------------------------------------------------------------------
ValidationError Traceback (most recent call last)
Cell In [46], line 1
----> 1 function(1, 15)
...
ValidationError: 1 validation error for Function
fraction
ensure this value is less than or equal to 1.0 (type=value_error.number.not_le; limit_value=1.0)
See the ConstrainedTypes section for more con*
variations.
I am a big fan of the type hinting in python, however I am curious if there is a way to specify a valid range of values for a given parameter using type hinting.
What I had in mind is something like
from typing import *
def function(
number: Union[float, int],
fraction: Float[0.0, 1.0] = 0.5 # give a hint that this should be between 0 and 1,
):
return fraction * number
I can imagine one can enforce this with an assertion, or perhaps specify what the valid range of values is within the docstring, but it feels like having something like Float[0.0, 1.0] would look more elegant.
Python 3.9 introduced typing.Annotated
:
In [75]: from typing import *
In [76]: from dataclasses import dataclass
In [77]: @dataclass
...: class ValueRange:
...: min: float
...: max: float
...:
In [78]: def function(
...: number: Union[float, int],
...: fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
...: ):
...: return fraction * number
...:
Like any other type hint it does not perform any runtime checks:
In [79]: function(1, 2)
Out[79]: 2
However you can implement your own runtime checks. The code below is just an example, it does not cover all cases and probably an overkill for your simple function:
In [88]: import inspect
In [89]: @dataclass
...: class ValueRange:
...: min: float
...: max: float
...:
...: def validate_value(self, x):
...: if not (self.min <= x <= self.max):
...: raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
...:
In [90]: def check_annotated(func):
...: hints = get_type_hints(func, include_extras=True)
...: spec = inspect.getfullargspec(func)
...:
...: def wrapper(*args, **kwargs):
...: for idx, arg_name in enumerate(spec[0]):
...: hint = hints.get(arg_name)
...: validators = getattr(hint, '__metadata__', None)
...: if not validators:
...: continue
...: for validator in validators:
...: validator.validate_value(args[idx])
...:
...: return func(*args, **kwargs)
...: return wrapper
...:
...:
In [91]: @check_annotated
...: def function_2(
...: number: Union[float, int],
...: fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
...: ):
...: return fraction * number
...:
...:
In [92]: function_2(1, 2)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-92-c9345023c025> in <module>
----> 1 function_2(1, 2)
<ipython-input-90-01115cb628ba> in wrapper(*args, **kwargs)
10 continue
11 for validator in validators:
---> 12 validator.validate_value(args[idx])
13
14 return func(*args, **kwargs)
<ipython-input-87-7f4ac07379f9> in validate_value(self, x)
6 def validate_value(self, x):
7 if not (self.min <= x <= self.max):
----> 8 raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
9
ValueError: 2 must be in range [0.0, 1.0]
In [93]: function_2(1, 1)
Out[93]: 1
If you can and you don’t mind using third-party packages, Pydantic provides Constrained Types. For your specific example, one of the constrained types is confloat
with the following parameters:
ge: float = None
: enforces float to be greater than or equal to the set valuelt: float = None
: enforces float to be less than the set value
In [35]: from pydantic import confloat
In [36]: def function(
...: number: Union[float, int],
...: fraction: confloat(ge=0.0, le=1.0) = 0.5,
...: ) -> float:
...: return fraction * number
If only used as a type hint, it doesn’t enforce it at runtime:
In [38]: function(1, 0)
Out[38]: 0
In [39]: function(1, 1.0)
Out[39]: 1.0
In [40]: function(1, 15)
Out[40]: 15
But you can use Pydantic’s validate_arguments
decorator which:
allows the arguments passed to a function to be parsed and validated using the function’s annotations before the function is called
In [41]: from pydantic import confloat, validate_arguments
In [42]: @validate_arguments
...: def function(
...: number: Union[float, int],
...: fraction: confloat(ge=0.0, le=1.0) = 0.5,
...: ) -> float:
...: return fraction * number
...:
In [43]: function(1, 0)
Out[43]: 0.0
In [44]: function(1, 1.0)
Out[44]: 1.0
In [45]: function(1, 0.37)
Out[45]: 0.37
In [46]: function(1, 15)
---------------------------------------------------------------------------
ValidationError Traceback (most recent call last)
Cell In [46], line 1
----> 1 function(1, 15)
...
ValidationError: 1 validation error for Function
fraction
ensure this value is less than or equal to 1.0 (type=value_error.number.not_le; limit_value=1.0)
See the ConstrainedTypes section for more con*
variations.