Some other recipes have been suggested to allow type checking by various means. Some of these require the use of type specification in a decorator itself. Others try to be much more elaborate in processing a large variety of annotations (but hence require much more and more convoluted code).
The recipe provided below is very short, and simply provides actual type checking of arguments and return values. It utilizes an unadorned decorator, rather than manufacture one that is parameterized by types or other arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | #!/usr/bin/env python3
"A decorator to use Python3 annotations to type-check inputs and outputs"
import functools
def typecheck(f):
@functools.wraps(f)
def decorated(*args, **kws):
for i, name in enumerate(f.__code__.co_varnames):
argtype = f.__annotations__.get(name)
# Only check if annotation exists and it is as a type
if isinstance(argtype, type):
# First len(args) are positional, after that keywords
if i < len(args):
assert isinstance(args[i], argtype)
elif name in kws:
assert isinstance(kws[name], argtype)
result = f(*args, **kws)
returntype = f.__annotations__.get('return')
if isinstance(returntype, type):
assert isinstance(result, returntype)
return result
return decorated
#################################
# Simple self-test and examples
#################################
def check(func, TESTS):
for test in TESTS:
try:
args = test[:-1]
expectation = test[-1]
print("TEST: %s%r -> float ... " % (func.__name__, args), end="")
result = func(*args)
if expectation == "SUCCEED":
print("Succeed as expected")
else:
print("Succeed incorrectly")
except AssertionError:
if expectation == "FAIL":
print("Failed as expected")
else:
print("Failed incorrectly")
if __name__ == "__main__":
@typecheck
def happy1(a:int, b:list, c:tuple=(1,2,3)) -> float:
return 3.14
@typecheck
def happy_with_nontype(a:int, b:list, x:"non-type", c:tuple=(1,2,3)) -> float:
return 3.14
@typecheck
def happy_wo_annotation(a:int, b, c:tuple=(1,2,3)) -> float:
return 3.14
@typecheck
def unhappy1(a:int, b:str) -> float:
return 314 # This can never succeed in return type
check(happy1, [
(17, ['a','b'], "SUCCEED"),
(17, ['a','b'], (4,5,6), "SUCCEED"),
(17.0, ['a','b'], (4,5,6), "FAIL"),
(17, ('a','b'), "FAIL")])
check(happy_with_nontype, [
(17, ['a','b'], "whatever", "SUCCEED"),
(17, ['a','b'], 0xDEADBEAF, (4,5,6), "SUCCEED"),
(17.0, ['a','b'], "whatever", (4,5,6), "FAIL"),
(17, ('a','b'), "whatever", "FAIL")])
check(happy_wo_annotation, [
(17, 'b', "SUCCEED"),
(17, ['a','b'], (4,5,6), "SUCCEED"),
(17.0, 'b', (4,5,6), "FAIL"),
(17, 'b', [4,5,6], "FAIL")])
check(unhappy1, [
(17, "x", 'FAIL'),
(1.7, "x", 'FAIL')])
|