Python typing validation
Question:
I would like to implement validation for Python 3.6 type annotation within my project.
I have a method that uses __annotations__
dict to check if all attributes of the class have the correct value. It works perfectly for basic types like int
, str
or bool
, but fails for more sophisticated elements like typing.Union
or typing.Optional
(which is also a Union).
The failure is caused by isinstance()
method within Union
object that throws TypeError
. I even cannot find a way to ensure that the annotation is a Union so I cannot validate if a value complies with a type.
The typing
module does not have any solution for it. Is there a way to validate if specified variable complies with typing.Union
?
Answers:
Yes. isinstance
and issubclass
were killed some time ago for cases like Union
.
The idea, as also stated in a comment on the issue by GvR is to implement your own version of issubclass/isinstance
that use some of the extra metadata attached to types:
>>> Union[int, str].__args__
(int, str)
>>> Union[int, str].__origin__
typing.Union
__args__
and __origin__
are available as of Python 3.6.3. They might not in earlier versions since typing is still provisional.
Until the internal interface for introspecting types is fleshed out and typing graduates from provisional status, you should expect breakage due to changes in the API.
Before using isinstance()
, you need to check if the annotation is an annotation from module typing
.
I do it like this:
def _is_instance(self, value: Any, annotation: type) -> bool:
str_annotation = str(annotation)
if self._is_typing_alias(str_annotation):
if self._is_supported_alias(str_annotation):
is_instance = self._get_alias_method(str_annotation)
if is_instance is not None:
return is_instance(value, annotation)
exception = TypeError('Alias is not supported!')
self._field_errors.append(TypingValidationError(
value_repr=get_value_repr(value), value_type=type(value),
annotation=annotation, exception=exception
))
return False
return super()._is_instance(value, annotation)
More details here: https://github.com/EvgeniyBurdin/validated_dc/blob/master/validated_dc.py
convtools models are based on typing and are validation first: docs | github.
from typing import Union, List
from convtools.contrib.models import DictModel, build
class ItemModel(DictModel):
value: Union[int, str]
obj, errors = build(
List[ItemModel], [{"value": 123}, {"value": "cde"}, {"value": 1.5}]
)
"""
>>> In [7]: errors
>>> Out[7]: {2: {'value': {'__ERRORS': {'type': 'float instead of int/str'}}}}
"""
obj, errors = build(List[ItemModel], [{"value": 123}, {"value": "cde"}])
"""
>>> In [9]: obj
>>> Out[9]: [ItemModel(value=123), ItemModel(value='cde')]
"""
Maybe I misunderstood the question, but, as of 3.10, isinstance worked for me, including on 3.10 style types (using |
for unions and |None
for optional).
class Check:
a : int
b : int|float
c : str|None
def __init__(self,a,b,c=None) -> None:
self.a, self.b, self.c = a,b,c
def validate(self):
for k, constraint in self.__annotations__.items():
v = getattr(self, k, None)
if not isinstance(v, constraint):
print(f"nn❌{k} : {v} does not match {constraint}")
return False
return True
def check(annotation, value):
print(f"nncheck({locals()})",end="")
return isinstance(value, annotation)
a = Check.__annotations__["a"]
b = Check.__annotations__["b"]
c = Check.__annotations__["c"]
print(f"✅ {check(a,3)}")
print(f"❌ {check(a,4.1)}")
print(f"✅ {check(b,4.1)}")
print(f"✅ {check(c,None)}")
print(f"❌ {check(b,None)}")
check = Check(a=1,b=2)
print("n",check.validate(), f"for {check.__dict__}",)
check = Check(a=1,b="2")
print("n",check.validate(), f"for {check.__dict__}","nn")
output:
check({'annotation': <class 'int'>, 'value': 3})✅ True
check({'annotation': <class 'int'>, 'value': 4.1})❌ False
check({'annotation': int | float, 'value': 4.1})✅ True
check({'annotation': str | None, 'value': None})✅ True
check({'annotation': int | float, 'value': None})❌ False
True for {'a': 1, 'b': 2, 'c': None}
❌b : 2 does not match int | float
False for {'a': 1, 'b': '2', 'c': None}
p.s. changed it to c : Optional[str]
and confirmed that worked as well.
p.p.s.
All is not rosy.
Having typing.Any
anywhere resulted in isinstance
throwing TypeError.
For now I’ve added a quick and dirty function to see if isinstance will work, for that annotation:
@functools.lru_cache
def can_isinstance(anno) -> bool:
""" check if isinstance is going to throw an error"""
try:
isinstance(None, anno)
return True
except (TypeError,) as e:
return False
....
myvalue = "xxx"
if can_isinstance(annotation):
if not isinstance(myvalue, annotation):
...
I would like to implement validation for Python 3.6 type annotation within my project.
I have a method that uses __annotations__
dict to check if all attributes of the class have the correct value. It works perfectly for basic types like int
, str
or bool
, but fails for more sophisticated elements like typing.Union
or typing.Optional
(which is also a Union).
The failure is caused by isinstance()
method within Union
object that throws TypeError
. I even cannot find a way to ensure that the annotation is a Union so I cannot validate if a value complies with a type.
The typing
module does not have any solution for it. Is there a way to validate if specified variable complies with typing.Union
?
Yes. isinstance
and issubclass
were killed some time ago for cases like Union
.
The idea, as also stated in a comment on the issue by GvR is to implement your own version of issubclass/isinstance
that use some of the extra metadata attached to types:
>>> Union[int, str].__args__
(int, str)
>>> Union[int, str].__origin__
typing.Union
__args__
and __origin__
are available as of Python 3.6.3. They might not in earlier versions since typing is still provisional.
Until the internal interface for introspecting types is fleshed out and typing graduates from provisional status, you should expect breakage due to changes in the API.
Before using isinstance()
, you need to check if the annotation is an annotation from module typing
.
I do it like this:
def _is_instance(self, value: Any, annotation: type) -> bool:
str_annotation = str(annotation)
if self._is_typing_alias(str_annotation):
if self._is_supported_alias(str_annotation):
is_instance = self._get_alias_method(str_annotation)
if is_instance is not None:
return is_instance(value, annotation)
exception = TypeError('Alias is not supported!')
self._field_errors.append(TypingValidationError(
value_repr=get_value_repr(value), value_type=type(value),
annotation=annotation, exception=exception
))
return False
return super()._is_instance(value, annotation)
More details here: https://github.com/EvgeniyBurdin/validated_dc/blob/master/validated_dc.py
convtools models are based on typing and are validation first: docs | github.
from typing import Union, List
from convtools.contrib.models import DictModel, build
class ItemModel(DictModel):
value: Union[int, str]
obj, errors = build(
List[ItemModel], [{"value": 123}, {"value": "cde"}, {"value": 1.5}]
)
"""
>>> In [7]: errors
>>> Out[7]: {2: {'value': {'__ERRORS': {'type': 'float instead of int/str'}}}}
"""
obj, errors = build(List[ItemModel], [{"value": 123}, {"value": "cde"}])
"""
>>> In [9]: obj
>>> Out[9]: [ItemModel(value=123), ItemModel(value='cde')]
"""
Maybe I misunderstood the question, but, as of 3.10, isinstance worked for me, including on 3.10 style types (using |
for unions and |None
for optional).
class Check:
a : int
b : int|float
c : str|None
def __init__(self,a,b,c=None) -> None:
self.a, self.b, self.c = a,b,c
def validate(self):
for k, constraint in self.__annotations__.items():
v = getattr(self, k, None)
if not isinstance(v, constraint):
print(f"nn❌{k} : {v} does not match {constraint}")
return False
return True
def check(annotation, value):
print(f"nncheck({locals()})",end="")
return isinstance(value, annotation)
a = Check.__annotations__["a"]
b = Check.__annotations__["b"]
c = Check.__annotations__["c"]
print(f"✅ {check(a,3)}")
print(f"❌ {check(a,4.1)}")
print(f"✅ {check(b,4.1)}")
print(f"✅ {check(c,None)}")
print(f"❌ {check(b,None)}")
check = Check(a=1,b=2)
print("n",check.validate(), f"for {check.__dict__}",)
check = Check(a=1,b="2")
print("n",check.validate(), f"for {check.__dict__}","nn")
output:
check({'annotation': <class 'int'>, 'value': 3})✅ True
check({'annotation': <class 'int'>, 'value': 4.1})❌ False
check({'annotation': int | float, 'value': 4.1})✅ True
check({'annotation': str | None, 'value': None})✅ True
check({'annotation': int | float, 'value': None})❌ False
True for {'a': 1, 'b': 2, 'c': None}
❌b : 2 does not match int | float
False for {'a': 1, 'b': '2', 'c': None}
p.s. changed it to c : Optional[str]
and confirmed that worked as well.
p.p.s.
All is not rosy.
Having typing.Any
anywhere resulted in isinstance
throwing TypeError.
For now I’ve added a quick and dirty function to see if isinstance will work, for that annotation:
@functools.lru_cache
def can_isinstance(anno) -> bool:
""" check if isinstance is going to throw an error"""
try:
isinstance(None, anno)
return True
except (TypeError,) as e:
return False
....
myvalue = "xxx"
if can_isinstance(annotation):
if not isinstance(myvalue, annotation):
...