Check a variable against Union type at runtime in Python 3.6
Question:
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?
Answers:
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
.
The existing accepted answer by MSeifert (https://stackoverflow.com/a/45959000/7433423) does not distinguish Union
s 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
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.
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.
I guess that Union is not a type itself, but a description of a type.
But we can request the type simply by using type(Union)
(also works in 2.7)
>>> from typing import Union
>>> type(Union)
typing.Union
>>> x = Union[int, str]
>>> isinstance(x, type(Union))
True
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?
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
.
The existing accepted answer by MSeifert (https://stackoverflow.com/a/45959000/7433423) does not distinguish Union
s 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
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.
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.
I guess that Union is not a type itself, but a description of a type.
But we can request the type simply by using type(Union)
(also works in 2.7)
>>> from typing import Union
>>> type(Union)
typing.Union
>>> x = Union[int, str]
>>> isinstance(x, type(Union))
True