59

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'
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Bertrand Caron
  • 2,525
  • 2
  • 22
  • 49
  • 3
    No you can't, but there's a similar question here: http://stackoverflow.com/questions/37973820/how-to-perform-type-checking-with-the-typing-python-module/43445558, and I tried to answer it. – max Apr 28 '17 at 02:51
  • @max Thanks for the attempt. I actually tried getting in touch with the `mypy` people through `gitter` and it seems like a similar feature is in the works, I'll see if I can get someone from the project to answer here and maybe update it as it moves forward. – Bertrand Caron Apr 28 '17 at 06:36
  • 1
    As I understand it, [`typing_inspect`](https://github.com/python/typing/pull/377/files) is not exactly what you are looking for; it's more about inspecting the types objects themselves. – Elazar Apr 28 '17 at 08:00
  • 1
    There's a very elaborate type checking implementation in this closely related question: [Validating detailed types in python dataclasses](//stackoverflow.com/q/50563546) – Aran-Fey Oct 03 '18 at 12:46
  • Thanks for that, seems like there is enough code in the answer to warrant a small (but very useful) package ;) – Bertrand Caron Oct 04 '18 at 22:04

4 Answers4

35

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])
aravindsagar
  • 3,852
  • 1
  • 16
  • 18
  • 7
    Better yet, typeguard's @typechecked decorator lets you automatically do typehint validation on all inputs and outputs to a function. Or, if you slap it on a class definition, it'll do runtime validation on all of its methods – crypdick Oct 20 '20 at 15:02
17

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"
Elazar
  • 20,415
  • 4
  • 46
  • 67
  • 3
    Yes. In an answer to a similar question, I came to this conclusion too (although I deleted those paragraphs because I thought my explanation was not very clear). I think the python `typing` / `PEP 484` type system is built for static type checking and is inappropriate for dynamic type checking. A practically useful dynamic type system *can* be built, but it will be very different (mostly much simpler) than `PEP 484`. And arguably, a pretty good one is already included with your off-the-shelf python interpreter. – max Apr 28 '17 at 08:39
  • "There is no such function in the typing module, and *most likely there won't ever be*." -- oops! – Noldorin Mar 07 '21 at 22:34
  • 2
    @Elazar: Nice answer! A bit of nitpicking: I don't think Rice's theorem is enough to prove that a type system is undecidable. Deciding whether a type system is decidable isn't that easy. :-) https://3fx.ch/typing-is-hard.html (I don't know if there is a well-defined type system for Python that has some kind of parametric polymorphism. If there is, chances are it's undecidable. But proving it might be a good deal of work.) – jcsahnwaldt Reinstate Monica Oct 27 '21 at 20:54
  • @jcsahnwaldtReinstateMonica I think there are two different issues here. The question whether arbitrary code has runtime type violation is always undecidable, since it is a nontrivial property. You are talking about the existence of a sound type-checking algorithm for a system with syntactic typing rules. These rules may or may not be truing-complete. – Elazar Feb 02 '22 at 10:48
7

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
Alex Paramonov
  • 2,630
  • 2
  • 23
  • 27
0

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))
MaVCArt
  • 745
  • 8
  • 21