Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check a variable against Union type at runtime in Python 3.6

I'm trying to write a function decorator that uses Python 3.6 type hints to check that a dictionary of arguments respects the type hints and if not raise an error with a clear description of the problem, to be used for HTTP APIs.

The problem is that when the function has a parameter using the Union type I can't check a variable against it at runtime.

For example, I have this function

from typing import Union
def bark(myname: str, descr: Union[int, str], mynum: int = 3) -> str:
    return descr + myname * mynum

I can do:

isinstance('Arnold', bark.__annotations__['myname'])

But not:

isinstance(3, bark.__annotations__['descr'])

Because Union cannot be used with isinstance or issubclass.

I couldn't find a way to check it using the type object. I tried to implement the check by myself but while bark.__annotations__['descr'] is shown as typing.Union[int, str] in the REPL I can't access the list of the types at runtime, if not using the ugly hack of examining bark.__annotations__['descr'].__repr__().

Is there a proper way to access this information? Or is it deliberately intended to not be easily accessible at runtime?

like image 914
Jacopofar Avatar asked Aug 30 '17 10:08

Jacopofar


People also ask

How do you check if a variable is a certain type Python?

Use the type() Function to Check Variable Type in Python To check the type of a variable, you can use the type() function, which takes the variable as an input. Inside this function, you have to pass either the variable name or the value itself. And it will return the variable data type.

How do you use union type in Python?

Union type; Union[X, Y] is equivalent to X | Y and means either X or Y. To define a union, use e.g. Union[int, str] or the shorthand int | str . Using that shorthand is recommended.

Is Python type checked at runtime?

Python types do not provide runtime type-checking.

What is Typevar in Python?

It is a type variable. Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function definitions.


4 Answers

In Python 3.8 and later, the approach suggested by MSeifert and Richard Xia can be improved by not using the undocumented attributes __origin__ and __args__. This functionality is provided by the new functions typing.get_args(tp) and typing.get_origin(tp):

>> from typing import Union, get_origin, get_args
>> x = Union[int, str]
>> get_origin(x), get_args(x)
(typing.Union, (<class 'int'>, <class 'str'>))
>> get_origin(x) is Union
True
>> isinstance(3, get_args(x))
True
>> isinstance('a', get_args(x))
True
>> isinstance([], get_args(x))
False

P.S.: I know that the question is about Python 3.6 (probably because this was the newest version at the time), but I arrived here when I searched for a solution as a Python 3.8 user. I guess that others might be in the same situation, so I thought that adding a new answer here makes sense.

like image 112
Frank Avatar answered Oct 23 '22 20:10

Frank


You could use the __args__ attribute of Union which holds a tuple of the "possible contents:

>>> from typing import Union

>>> x = Union[int, str]
>>> x.__args__
(int, str)
>>> isinstance(3, x.__args__)
True
>>> isinstance('a', x.__args__)
True

The __args__ argument is not documented so it could be considered "messing with implementation details" but it seems like a better way than parsing the repr.

like image 33
MSeifert Avatar answered Oct 23 '22 19:10

MSeifert


The existing accepted answer by MSeifert (https://stackoverflow.com/a/45959000/7433423) does not distinguish Unions from other generic types, and it is difficult to determine at runtime whether a type annotation is a Union or some other generic type like Mapping due to the behavior of isinstance() and issubclass() on parameterized Union types.

It appears that generic types will have an undocumented __origin__ attribute which will contain a reference to the original generic type used to create it. Once you have confirmed that the type annotation is a parameterized Union, you can then use the also undocumented __args__ attribute to get the type parameters.

>>> from typing import Union
>>> type_anno = Union[int, str]
>>> type_anno.__origin__ is Union
True
>>> isinstance(3, type_anno.__args__)
True
>>> isinstance('a', type_anno.__args__)
True
like image 25
Richard Xia Avatar answered Oct 23 '22 19:10

Richard Xia


You can use the typeguard module which can be installed with pip. It provides you with a function check_argument_types or a function decorator @typechecked. which should do your runtime type checking for you: https://github.com/agronholm/typeguard

from typing import Union
from typeguard import check_argument_types, typechecked

def check_and_do_stuff(a: Union[str, int]) -> None:
    check_argument_types() 
    # do stuff ...

@typechecked
def check_decorator(a: Union[str, int]) -> None:
    # do stuff ...

check_and_do_stuff("hello")
check_and_do_stuff(42)
check_and_do_stuff(3.14)  # raises TypeError

If you want to check a type of a single variable for a different reason, you can use typeguard's check_type function directly:

from typing import Union
from typeguard import check_type

MyType = Union[str, int]

check_type("arg", "string", MyType, None)  # OK
check_type("arg", 42, MyType, None)  # OK
check_type("arg", 3.5, MyType, None)  # raises TypeError

The "arg" and None arguments are unused in this example. Note that the check_type function is not documented as a public function of this module so its API may be subject to change.

like image 9
Jindra Helcl Avatar answered Oct 23 '22 19:10

Jindra Helcl