Python check isinstance with typing [duplicate] - python

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'

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])

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"

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

Related

Python Unit Test to Assert Type Annotation of Object

In Python versions <3.11 where the assert_type (source) isn't available, how does one assert a type annotation via the unittest TestCase class? Example of the problem:
from typing import List
from unittest import TestCase
def dummy() -> List[str]:
return ["one", "two", "three"]
class TestDummy(TestCase):
def test_dummy(self):
self.assertEqual(
List[str],
type(dummy())
)
The test fails with the following output:
<class 'list'> != typing.List[str]
Expected :typing.List[str]
Actual :<class 'list'>
<Click to see difference>
Traceback (most recent call last):
File "C:\Users\z\dev\mercata\scratch\mock_testing.py", line 12, in test_dummy
self.assertEqual(
AssertionError: typing.List[str] != <class 'list'>
The approach I currently use is as follows:
data = dummy()
self.assertTrue(
type(data) == list
)
self.assertTrue(all([
type(d) == str for d in data
]))
This works but requires iterating the entirety of the object which is unwieldy with larger datasets. Is there a more efficient approach for Python versions <3.11 (not requiring a third-party package)?
assert_type is used to ask a static type checker to confirm a value is of some type. At normal runtime this method doesn't do anything. If you want to use it, then you should use static analysis tooling, for example mypy or pyright. Checking assertEqual is a runtime operation, and unlike some languages, instances of generics in python do not retain their type info at runtime, which is why the class is being shown as the standard <class 'list'> and not the generic one from the method type annotation.
Because assert_type doesn't perform anything at runtime, it will not check for the contents of the actual list. It is used to add an explicit typecheck into the code, and only useful if all of the inputs for how a variable was constructed have been properly type checked as well. So it would also not be useful within unit testing as you have it.
For example, the following script only produces one error:
from typing import assert_type
def dummy() -> list[str]:
return [1]
res = dummy()
assert_type(res, list[str])
(venv) $ mypy test.py
test.py:4: error: List item 0 has incompatible type "int"; expected "str" [list-item]
Found 1 error in 1 file (checked 1 source file)
This detects the error of an int list being returned by dummy, but the assert_type succeeds because it would be correct if dummy had respected its contract.
If we fixed dummy like below, then at this point we would get the expected assert_type error:
from typing import assert_type
def dummy() -> list[int]:
return [1]
res = dummy()
assert_type(res, list[str])
(venv) $ mypy test.py
test.py:7: error: Expression is of type "List[int]", not "List[str]" [assert-type]
Found 1 error in 1 file (checked 1 source file)
While I agree with the general sentiment the commenters have expressed that this type of thing should probably be left to static type checkers rather than unit tests, just for academic purposes, you can construct your own assertion without too much effort.
Something like list[str] is a specified version of the generic type list. By subscripting a type like like list, you are actually calling its __class_getitem__ method, which returns the specified type. The type argument is actually stored and the typing module provides the get_args/get_origin functions to extract more detailed type information from generic types at runtime.
from typing import get_args
print(get_args(list[str])) # (<class 'str'>,)
The problem is more that any concrete list object (like ["one", "two", "three"]) does not store any information about the type of the items it holds (for obvious reasons). This means, at runtime, we would have to check the type of the elements ourselves.
The question thus becomes how pedantic you want your check to be. Lists for example can be as long as you want them to be (or as your memory allows). If you a list object of a million elements, do you really want to check each and every one of them? A possible compromise might be to only check the very first element's type or something like that.
Here is an example of a function checking arbitrary iterable types that are parameterized by "regular" types only (i.e. not something like list[tuple[int]]):
from collections.abc import Iterable
from types import GenericAlias
from typing import Union, cast, get_origin, get_args
def is_of_iter_type(
value: object,
type_: Union[type[Iterable[object]], GenericAlias],
pedantic: bool = False,
) -> bool:
if isinstance(type_, type): # something like unspecified `list`
return isinstance(value, type_)
if isinstance(type_, GenericAlias): # a specified generic like `list[str]`
origin, args = get_origin(type_), get_args(type_)
if not isinstance(origin, type) or not issubclass(origin, Iterable):
raise TypeError
arg = cast(type, args[0])
if not isinstance(arg, type): # type arg is a type var or another generic alias
raise TypeError
if not isinstance(value, origin):
return False
if pedantic:
return all(isinstance(item, arg) for item in value)
else:
return isinstance(next(iter(value)), arg)
raise TypeError
Note also that depending on what iterable you actually pass to this function, it may be a terrible idea to (try to) consume the resulting iterator (via next or all). It would be up to you to ensure that this does not have any bad side effects.
Here is a demo:
print(is_of_iter_type("a", list[str])) # False
print(is_of_iter_type(["a"], list[str])) # True
print(is_of_iter_type(["a"], list)) # True
print(is_of_iter_type(["a", 1], list[str])) # True
print(is_of_iter_type(["a", 1], list[str], pedantic=True)) # False
To incorporate it into a unittest.TestCase you could do this:
...
from unittest import TestCase
class ExtendedTestCase(TestCase):
def assert_is_of_iter_type(
self,
value: object,
type_: Union[type[Iterable[object]], GenericAlias],
pedantic: bool = False,
) -> None:
if not is_of_iter_type(value, type_, pedantic=pedantic):
self.fail(f"{value} is not of type {type_}")
def test(self) -> None:
self.assert_is_of_iter_type(["a", 1], list[str], pedantic=True)
But again, this is most likely not a good idea because something like mypy in --strict mode will probably do a better job at ensuring type safety throughout your code than you could hope to do at runtime. Meaning if you declare def dummy() -> list[str]: ..., but the in the function body you return ["a", 1], then mypy will pick that up and yell at you. Thus, there would be no need for such a test.

Python dataclass evaluating type-hinted Dict fields [duplicate]

I'm trying to generate some JavaScript based on the type annotations I have provided in on some Python functions by using the signature() function in the inspect module.
This part works as I expect when the type is a simple builtin class:
import inspect
def my_function() -> dict:
pass
signature = inspect.signature(my_function)
signature.return_annotation is dict # True
Though I'm not sure how to unwrap and inspect more complex annotations e.g:
from typing import List
import inspect
def my_function() -> List[int]:
pass
signature = inspect.signature(my_function)
signature.return_annotation is List[int] # False
Again similar problem with forward referencing a custom class:
def my_function() -> List['User']:
pass
...
signature.return_annotation # typing.List[_ForwardRef('User')]
What I'm looking to get out is something like this - so I can branch appropriately while generating the JavaScript:
type = signature.return_annotation... # list
member_type = signature.return_annotation... # int / 'User'
Thanks.
Python 3.8 provides typing.get_origin() and typing.get_args() for this!
assert get_origin(Dict[str, int]) is dict
assert get_args(Dict[int, str]) == (int, str)
assert get_origin(Union[int, str]) is Union
assert get_args(Union[int, str]) == (int, str)
See https://docs.python.org/3/library/typing.html#typing.get_origin
List is not a map of types to GenericMeta, despite the syntax. Each access to it generates a new instance:
>>> [ id(List[str]) for i in range(3) ]
[33105112, 33106872, 33046936]
This means that even List[int] is not List[int]. To compare two instances, you have multiple options:
Use ==, i.e., signature.return_annotation == List[int].
Store an instance of your type in a global variable and check against that, i.e.,
a = List[int]
def foo() -> a:
pass
inspect.signature(foo).return_annotation is a
Use issubclass. The typing module defines that. Note that this might do more than you'd like, make sure to read the _TypeAlias documentation if you use this.
Check against List only and read the contents yourself. Though the property is internal, it is unlikely that the implementation will change soon: List[int].__args__[0] contains the type argument starting from Python 3.5.2, and in earlier versions, its List[int].__parameters__[0].
If you'd like to write generic code for your exporter, then the last option is probably best. If you only need to cover a specific use case, I'd personally go with using ==.
Take note, this applies to Python 3.5.1
For Python 3.5.2 take a look at phillip's answer.
You shouldn't be checking with the identity operator as Phillip stated, use equality to get this right.
To check if a hint is a subclass of a list you could use issubclass checks (even though you should take note that this can be quirky in certain cases and is currently worked on):
issubclass(List[int], list) # True
To get the members of a type hint you generally have two watch out for the cases involved.
If it has a simple type, as in List[int] the value of the argument is located in the __parameters__ value:
signature.return_annotation.__parameters__[0] # int
Now, in more complex scenarios i.e a class supplied as an argument with List[User] you must again extract the __parameter__[0] and then get the __forward_arg__. This is because Python wraps the argument in a special ForwardRef class:
d = signature.return_annotation.__parameter__[0]
d.__forward_arg__ # 'User'
Take note, you don't need to actually use inspect here, typing has a helper function named get_type_hints that returns the type hints as a dictionary (it uses the function objects __annotations__ attribute).

Unpacking Python's Type Annotations

I'm trying to generate some JavaScript based on the type annotations I have provided in on some Python functions by using the signature() function in the inspect module.
This part works as I expect when the type is a simple builtin class:
import inspect
def my_function() -> dict:
pass
signature = inspect.signature(my_function)
signature.return_annotation is dict # True
Though I'm not sure how to unwrap and inspect more complex annotations e.g:
from typing import List
import inspect
def my_function() -> List[int]:
pass
signature = inspect.signature(my_function)
signature.return_annotation is List[int] # False
Again similar problem with forward referencing a custom class:
def my_function() -> List['User']:
pass
...
signature.return_annotation # typing.List[_ForwardRef('User')]
What I'm looking to get out is something like this - so I can branch appropriately while generating the JavaScript:
type = signature.return_annotation... # list
member_type = signature.return_annotation... # int / 'User'
Thanks.
Python 3.8 provides typing.get_origin() and typing.get_args() for this!
assert get_origin(Dict[str, int]) is dict
assert get_args(Dict[int, str]) == (int, str)
assert get_origin(Union[int, str]) is Union
assert get_args(Union[int, str]) == (int, str)
See https://docs.python.org/3/library/typing.html#typing.get_origin
List is not a map of types to GenericMeta, despite the syntax. Each access to it generates a new instance:
>>> [ id(List[str]) for i in range(3) ]
[33105112, 33106872, 33046936]
This means that even List[int] is not List[int]. To compare two instances, you have multiple options:
Use ==, i.e., signature.return_annotation == List[int].
Store an instance of your type in a global variable and check against that, i.e.,
a = List[int]
def foo() -> a:
pass
inspect.signature(foo).return_annotation is a
Use issubclass. The typing module defines that. Note that this might do more than you'd like, make sure to read the _TypeAlias documentation if you use this.
Check against List only and read the contents yourself. Though the property is internal, it is unlikely that the implementation will change soon: List[int].__args__[0] contains the type argument starting from Python 3.5.2, and in earlier versions, its List[int].__parameters__[0].
If you'd like to write generic code for your exporter, then the last option is probably best. If you only need to cover a specific use case, I'd personally go with using ==.
Take note, this applies to Python 3.5.1
For Python 3.5.2 take a look at phillip's answer.
You shouldn't be checking with the identity operator as Phillip stated, use equality to get this right.
To check if a hint is a subclass of a list you could use issubclass checks (even though you should take note that this can be quirky in certain cases and is currently worked on):
issubclass(List[int], list) # True
To get the members of a type hint you generally have two watch out for the cases involved.
If it has a simple type, as in List[int] the value of the argument is located in the __parameters__ value:
signature.return_annotation.__parameters__[0] # int
Now, in more complex scenarios i.e a class supplied as an argument with List[User] you must again extract the __parameter__[0] and then get the __forward_arg__. This is because Python wraps the argument in a special ForwardRef class:
d = signature.return_annotation.__parameter__[0]
d.__forward_arg__ # 'User'
Take note, you don't need to actually use inspect here, typing has a helper function named get_type_hints that returns the type hints as a dictionary (it uses the function objects __annotations__ attribute).

How to check type compatibility when using the typing module?

I'm using Python PEP484 type hints to write a type-checker for a DSL I've written in Python. If I have a function expecting type T for one of its arguments, and it's being called with an expression of type S, how can I check whether the call is valid? Is using issubclass(S, T) enough? If so, why does mypy have such a complicated is_subtype check? Or should I just use the mypy version?
Edit: Here's an example to clarify what I mean. The DSL has a function defined as:
T = TypeVar('T', float, str)
def op_add(operand1: T, operand2: T) -> T:
"Number addition or string concatenation."
# In this DSL, `+` cannot be used with lists
return operand1 + operand2 # Rely on Python overloading of `+`
Then a user types in an expression which is parsed into a syntax tree, with a branch that could be: node = OperatorNode('+', Literal([5.0]), Variable("abc")). We don't know the value of the abc variable yet, but lists can never be used with +, so I want to raise a TypeError to alert the user.
If I do issubclass(typing.List[float], var), that gives me False, so I can raise an error right away. My question is whether this check is guaranteed to work across cases as I build out the DSL, or if I need to use a more complex check like mypy
issubclass check is sufficient if neither argument to issubclass includes constructs from typing module such as Union, Callable, Any, generics, etc.
typing constructs exist in python runtime as a shadow of their true form, that is they don't support many of the operations that make sense conceptually:
issubclass(List[int], List[int]) # runtimem error
issubclass(List[int], List) # True (as expected)
issubclass(str, Union[str]) # runtime error
issubclass(Union[str], str) # True (as expected)
issubclass(Union[int, str], str) # runtime error
Sometimes issubclass will work with typing constructs, but in general, it may raise an exception or give an incorrect answer; you'll need to figure out what to do on a case by case basis.
mypy has a more complicated is_subtype because it does need to handle all the typing constructs, and even then there is still some work to be done there.

Is it possible to make python throw errors if the type of the argument passed to the annotated function doesn't match the one specified?

One of the new features in python3.5 is type hinting. For example the code below is valid now:
def greeting(name: str) -> str:
return 'Hello ' + name
But, as I understand, it doesn't check anything by itself and also is interpreted absolutely the same way as this:
def greeting(name):
return 'Hello ' + name
and was implemented mostly to help static analyzers (and to make code that is easier to understand). But is there (or is planned to be implemented in the future) any way (maybe by using some third-party libraries) to make python throw errors when an argument of an invalid type is passed to a function with annotated argument types (using only type-hinting syntax)?
Type hints implement PEP 0484 which explicitly lists as a non-goal:
While the proposed typing module will contain some building blocks for
runtime type checking -- in particular the get_type_hints() function
-- third party packages would have to be developed to implement specific runtime type checking functionality, for example using
decorators or metaclasses. Using type hints for performance
optimizations is left as an exercise for the reader.
From this it seems to follow that the Python developers have no plan to add the functionality that you seek. The quote mentions decorators and that does seem the way to go. In concept it seems straightforward -- the decorator would use get_type_hints() on the function to be decorated and would iterate through the arguments, checking their types against any hints, either throwing an error if there is a clash or simply passing on the arguments to the function. This would be similar to pzelasko's answer but with the decorator using the hints to automatically handle the boiler-plate code. The simplest approach would be to simply vet the arguments, though you should also be able to make a decorator which would raise an error if the return type clashes with the hint. I don't yet have Python 3.5 and don't have the time to pursue it -- but it seems like a good learning exercise for someone who wants to learn about both decorators and type hints. Perhaps you can be one of the "third parties" the PEP alludes to.
If you use Python3.6 annotations, you can use typeguard decorators:
https://typeguard.readthedocs.io/en/latest/userguide.html#using-the-decorator
NB: This should only be a "debug" or "testing" tool, not a production one.
Thus, they advise to add the -O option to python to run without in production.
It does not check inline variable annotation checks automatically, only function parameters, function return and object types.
From the doc:
from typeguard import typechecked
#typechecked
def some_function(a: int, b: float, c: str, *args: str) -> bool:
...
return retval
#typechecked
class SomeClass:
# All type annotated methods (including static and class methods and properties)
# are type checked.
# Does not apply to inner classes!
def method(x: int) -> int:
...
You may also automate this to all the functions with:
with install_import_hook('myapp'):
from myapp import some_module
I think the simplest way is to check the type:
def greeting(name):
if not isinstance(name, str):
raise TypeError('Expected str; got %s' % type(name).__name__)
return 'Hello ' + name
Since the release of python 3.7, it has been possible to do this using functools.singledispatch.
from functools import singledispatch
#singledispatch
def greet(arg: object):
raise NotImplementedError(f"Don't know how to greet {type(arg)}")
#greet.register
def _(arg: str):
print(f"Hello, {arg}!")
#greet.register
def _(arg: int):
print(', '.join("Hello" for _ in range(arg)), "!")
greet("Bob") # string implementation is called — prints "Hello, Bob!"
greet(4) # int implementation is called — prints "Hello, Hello, Hello, Hello!"
greet(["Alice, Bob"]) # no list implementation, so falls back to the base implementation — will raise an exception
In the above example, a base implementation is registered that will raise NotImplementedError. This is the "fallback" implementation that is returned if none of the other implementations is suitable.
Following the definition of the base implementation, an arbitrary number of type-specific implementations can be registered, as shown in the example — the function behaves completely differently depending on the type of the argument that is fed to it, and the #singledispatch method uses type annotations to register a certain implementation of a function with a certain type.
A singledispatch function can have any number of arguments, but only the type annotation for the first argument is relevant to which implementation is called.
One can use the library https://github.com/h2oai/typesentry for doing this on runtime.

Categories