isinstance with string representing types
Question:
isinstance("my string", "str | int")
isinstance("my string", "list")
Is there a way to check the type of a variable ("my string"
in this case) based on a valid type which is stored as a string?
I don’t need to import the type, I know it’s builtin (int / str / float / etc).
The naive approach would be to call eval()
, but that’s of course unsafe. Is there a safe way to do this other than using regex/string splitting to extract individual types and matching them to their evaluated types in a huge if/else block?
Answers:
For built-in types, you can do a lookup in the builtins
module:
>>> import builtins
>>> getattr(builtins, 'str')
<class 'str'>
And for classes you’ve defined in your own code there’s the globals()
dictionary:
>>> class Foo:
... pass
...
>>> globals()['Foo']
<class '__main__.Foo'>
Note that this doesn’t handle union or generic types.
Here’s a fairly convoluted way to avoid using eval
(directly; see below) and using the existing machinery to evaluate the type hint.
def foo(x):
pass
foo.__annotations__['x'] = "str|int"
assert typing.get_type_hints(foo)['x'] == (str|int)
Ultimately, typing.ForwardRef
itself uses eval
to evaluate "str|int"
into str|int
, but either
- There are safeguards against evaluating arbitrary expression that makes this safer than using
eval
directly, or
- There are no safeguards, but you’ll benefit from any future changes to the
typing
module.
fast_eval
For Python 3.6+
I would suggest to use the following declaration of fast_eval
as defined below, which is the fastest possible implementation which can satisfy the ask in the question above.
def fast_eval(val: Any, annotation: str) -> bool:
cls_name = val.__class__.__name__
if '|' in annotation:
for tp in annotation.split('|'):
if tp.strip() == cls_name:
return True
return False
return annotation.strip() == cls_name
Examples:
>>> fast_eval("hey", "int | str")
True
>>> fast_eval("hey", "int | bool")
False
>>> fast_eval("world", "int | list | datetime | BLAH")
False
>>> fast_eval(["world"], "int | list | datetime | BLAH")
True
Performance
Studies show, that it’s more than 10x faster than the most straightforward implementation of:
Note: The following works in Python 3.10+ only!
isinstance("hey", eval("int | str"))
Benchmark code:
# Requires: Python 3.10+
import builtins
from datetime import datetime
from timeit import timeit
from typing import Any
def fast_eval(val: Any, annotation: str) -> bool:
cls_name = val.__class__.__name__
if '|' in annotation:
for tp in annotation.split('|'):
if tp.strip() == cls_name:
return True
return False
return annotation.strip() == cls_name
def eval_with_type(val: Any, annotation: str, __globals=None, t=None) -> bool:
if __globals is None:
__globals = {}
if t is None:
t = type(val)
if '|' in annotation:
for tp in annotation.split('|'):
if eval_with_type(val, tp, __globals, t):
return True
return False
annotation = annotation.strip()
try: # is a BUILTIN? like (str, bool)
return t is getattr(builtins, annotation)
except AttributeError:
# NO, sir! instead, like (datetime, UUID) or user-defined class
try:
return t is __globals[annotation]
except KeyError as k:
raise TypeError(f'no such type is defined (in globals): {k}')
# asserts
assert True is fast_eval("hey", "int | datetime | str") is eval_with_type("hey", "int | datetime | str", globals())
assert False is fast_eval("hey", "int | datetime | bool") is eval_with_type("hey", "int | datetime | bool", globals())
# timings
print('fast_eval: ', timeit('fast_eval("hey", "int | datetime | str")', globals=globals()))
print('eval_with_type: ', timeit('eval_with_type("hey", "int | datetime | str", globals())', globals=globals()))
print('eval: ', timeit('isinstance("hey", eval("int | datetime | str", globals()))', globals=globals()))
Results on my Mac OS:
fast_eval: 0.3017798329819925
eval_with_type: 1.1461862919968553
eval: 3.5175461250473745
isinstance("my string", "str | int")
isinstance("my string", "list")
Is there a way to check the type of a variable ("my string"
in this case) based on a valid type which is stored as a string?
I don’t need to import the type, I know it’s builtin (int / str / float / etc).
The naive approach would be to call eval()
, but that’s of course unsafe. Is there a safe way to do this other than using regex/string splitting to extract individual types and matching them to their evaluated types in a huge if/else block?
For built-in types, you can do a lookup in the builtins
module:
>>> import builtins
>>> getattr(builtins, 'str')
<class 'str'>
And for classes you’ve defined in your own code there’s the globals()
dictionary:
>>> class Foo:
... pass
...
>>> globals()['Foo']
<class '__main__.Foo'>
Note that this doesn’t handle union or generic types.
Here’s a fairly convoluted way to avoid using eval
(directly; see below) and using the existing machinery to evaluate the type hint.
def foo(x):
pass
foo.__annotations__['x'] = "str|int"
assert typing.get_type_hints(foo)['x'] == (str|int)
Ultimately, typing.ForwardRef
itself uses eval
to evaluate "str|int"
into str|int
, but either
- There are safeguards against evaluating arbitrary expression that makes this safer than using
eval
directly, or - There are no safeguards, but you’ll benefit from any future changes to the
typing
module.
fast_eval
For Python 3.6+
I would suggest to use the following declaration of fast_eval
as defined below, which is the fastest possible implementation which can satisfy the ask in the question above.
def fast_eval(val: Any, annotation: str) -> bool:
cls_name = val.__class__.__name__
if '|' in annotation:
for tp in annotation.split('|'):
if tp.strip() == cls_name:
return True
return False
return annotation.strip() == cls_name
Examples:
>>> fast_eval("hey", "int | str")
True
>>> fast_eval("hey", "int | bool")
False
>>> fast_eval("world", "int | list | datetime | BLAH")
False
>>> fast_eval(["world"], "int | list | datetime | BLAH")
True
Performance
Studies show, that it’s more than 10x faster than the most straightforward implementation of:
Note: The following works in Python 3.10+ only!
isinstance("hey", eval("int | str"))
Benchmark code:
# Requires: Python 3.10+
import builtins
from datetime import datetime
from timeit import timeit
from typing import Any
def fast_eval(val: Any, annotation: str) -> bool:
cls_name = val.__class__.__name__
if '|' in annotation:
for tp in annotation.split('|'):
if tp.strip() == cls_name:
return True
return False
return annotation.strip() == cls_name
def eval_with_type(val: Any, annotation: str, __globals=None, t=None) -> bool:
if __globals is None:
__globals = {}
if t is None:
t = type(val)
if '|' in annotation:
for tp in annotation.split('|'):
if eval_with_type(val, tp, __globals, t):
return True
return False
annotation = annotation.strip()
try: # is a BUILTIN? like (str, bool)
return t is getattr(builtins, annotation)
except AttributeError:
# NO, sir! instead, like (datetime, UUID) or user-defined class
try:
return t is __globals[annotation]
except KeyError as k:
raise TypeError(f'no such type is defined (in globals): {k}')
# asserts
assert True is fast_eval("hey", "int | datetime | str") is eval_with_type("hey", "int | datetime | str", globals())
assert False is fast_eval("hey", "int | datetime | bool") is eval_with_type("hey", "int | datetime | bool", globals())
# timings
print('fast_eval: ', timeit('fast_eval("hey", "int | datetime | str")', globals=globals()))
print('eval_with_type: ', timeit('eval_with_type("hey", "int | datetime | str", globals())', globals=globals()))
print('eval: ', timeit('isinstance("hey", eval("int | datetime | str", globals()))', globals=globals()))
Results on my Mac OS:
fast_eval: 0.3017798329819925
eval_with_type: 1.1461862919968553
eval: 3.5175461250473745