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?

Asked By: Djent

||

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

Answered By: Evgeniy_Burdin

You can use Unions from typing as here: pydantic

Answered By: asu

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')]
"""
Answered By: westandskif

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):
           ...
Answered By: JL Peyret