1
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?

Some Guy
  • 576
  • 1
  • 4
  • 17
  • beware: `eval` for the first one only works in Python 3.10+, which is highly unfeasible. – rv.kvetch Mar 24 '23 at 14:34
  • see also: [(my) related question](https://stackoverflow.com/questions/69606986/regex-matching-separated-values-for-union-types) – rv.kvetch Mar 24 '23 at 14:35
  • You don't have a type stored as a string. You have a string which *`mypy`* can interpret as `int | str`. `eval` works because it's a superset of what you want, which is to parse and evaluate a type expression specifically, not an arbitrary Python expression. Depending on where your type-string comes from, it may be safe and perfectly fine to use `eval`. – chepner Mar 24 '23 at 14:41
  • @chepner that's not true for modern python. isinstance, for example, accepts built-in type unions natively as of python 3.10, and `int | str` can also be used as a function return signature both when wrapped in quotes (`"int | str"`) and without them. It's as valid a signature as they get, which is why I was expecting there to be a safe built-in way to evaluate it natively. – Some Guy Mar 24 '23 at 14:47
  • (Ultimately, `eval` is what `typing.ForwardRef` uses for the string, which, frankly, concerns me a little. `def f(x: "3+5"): pass`, then `typing.get_type_hints(f)['x'] == 8`. – chepner Mar 24 '23 at 14:47
  • @SomeGuy check out my answer below. this is an alternate approach which should hopefully work as well (fastest version I could come up with) – rv.kvetch Mar 24 '23 at 17:21

3 Answers3

4

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.

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • I was struggling with this until I realized that you need to import `builtins` and access that if you're running code in a module, because your example only works in the interpreter. If you update your answer to account for that I'll accept this. – Some Guy Mar 24 '23 at 14:58
0

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.
chepner
  • 497,756
  • 71
  • 530
  • 681
0

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
rv.kvetch
  • 9,940
  • 3
  • 24
  • 53