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?

Asked By: Some Guy

||

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.

Answered By: Samwise

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

  1. There are safeguards against evaluating arbitrary expression that makes this safer than using eval directly, or
  2. There are no safeguards, but you’ll benefit from any future changes to the typing module.
Answered By: chepner

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
Answered By: rv.kvetch
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.