Bertrand Caron
Bertrand Caron

Reputation: 2657

Python >=3.5: Checking type annotation at runtime

Does the typing module (or any other module) exhibit an API to typecheck a variable at runtime, similar to isinstance() but understanding the type classes defined in typing?

I'd like to be to run something akin to:

from typing import List
assert isinstance([1, 'bob'], List[int]), 'Wrong type'

Upvotes: 68

Views: 33657

Answers (5)

JokeUrSelf
JokeUrSelf

Reputation: 51

As far as I know, there is no built-in solution, so I've built a nested_type_checker package to check for a parametrized (nested) type. To use it, install it via console:

pip install nested_type_checker

imoprt is_object_of_type function:

from nested_type_checker import is_object_of_type

obj = [123, ({"": True},)]

CorrectType = list[int | tuple[dict[str, bool]]]
WrongType = list[bool | tuple[dict[str, bool]]]

a = is_object_of_type(obj, CorrectType)
b = is_object_of_type(obj, WrongType)

print(a)  # outputs True
print(b)  # outputs False

I hope it helps.

Upvotes: 1

MaVCArt
MaVCArt

Reputation: 793

the inspect module can resolve this pretty easily using vanilla python - no external modules required :)

It's a bit simplistic, granted; it probably won't work for deeply nested types (like dictionaries that need a given key/value type) but you might be able to expand this using the "typing" library.

import inspect


def enforce_type_annotation(fn):
    parameters = inspect.signature(fn).parameters
    param_keys = list(parameters.keys())

    def wrapper(*args, **kwargs):
        errors = list()

        # -- iterate over positionals
        for i in range(len(args)):
            param = parameters[param_keys[i]]
            value = args[i]

            # -- if the parameter is not annotated, don't validate.
            if not param.annotation:
                continue

            if not isinstance(value, param.annotation):
                errors.append(
                    f'Positional argument {param} was given type {type(value)} but expected {param.annotation}!'
                )

        # -- this might throw a KeyError if an incorrect argument is provided
        for key, value in kwargs.items():
            param = parameters[key]
            value = kwargs[key]

            # -- if the parameter is not annotated, don't validate.
            if not param.annotation:
                continue

            if not isinstance(value, param.annotation):
                errors.append(
                    f'Keyword argument {param} was given type {type(value)} but expected {param.annotation}!'
                )

        if len(errors):
            raise TypeError('\n'.join(errors))

        return fn(*args, **kwargs)

    return wrapper


@enforce_type_annotation
def foo(bar: bool, barry: str = None):
    return "hello world"


# -- works - keyword arguments remain optional
print(foo(True))

# -- works - all types were passed correctly
print(foo(True, 'Hello'))

# -- does not work, keyword arguments may also be passed as positional
print(foo(True, 1))

# -- does not work, "barry" expects a string
print(foo(True, barry=1))

Upvotes: 0

Alex Paramonov
Alex Paramonov

Reputation: 2710

This is what I have discovered recently, basically this decorator does type checking at runtime raising exception if some type definition did not match. It can also do type checking for nested types (dict of strings, etc)

https://github.com/FelixTheC/strongtyping

Example:

from strongtyping.strong_typing import match_typing

@match_typing
def func_a(a: str, b: int, c: list):
   ...

func_a('1', 2, [i for i in range(5)])
# >>> True

func_a(1, 2, [i for i in range(5)])
# >>> will raise a TypeMismatch Exception

Upvotes: 7

aravindsagar
aravindsagar

Reputation: 4222

I was looking for something similar and found the library typeguard. This can automatically do runtime type checks wherever you want. Checking types directly as in the question is also supported. From the docs,

from typeguard import check_type

# Raises TypeError if there's a problem
check_type('variablename', [1234], List[int])

Upvotes: 48

Elazar
Elazar

Reputation: 21585

There is no such function in the typing module, and most likely there won't ever be.

Checking whether an object is an instance of a class - which only means "this object was created by the class' constructor" - is a simple matter of testing some tagging.

However, checking whether an object is an "instance" of a type is not necessarily decidable:

assert isinstance(foo, Callable[[int], str]), 'Wrong type'

Although it is easy to inspect the typing annotations of foo (assuming it's not a lambda), checking whether it complies to them is generally undecidable, by Rice's theorem.

Even with simpler types, such as List[int] the test will easily become far too inefficient to be used for anything but the smallest toy examples.

xs = set(range(10000))
xs.add("a")
xs.pop()
assert isinstance(xs, Set[int]), 'Wrong type'

The trick that allows type checker to perform this operation in a relatively efficient way, is to be conservative: the type checker tries to prove that foo always return int. If it fails, it rejects the program, even though the program may be valid, i.e. this function is likely to be rejected, although it is perfectly safe:

def foo() -> int:
    if "a".startswith("a"):
        return 1
    return "x"

Upvotes: 18

Related Questions