Skip to content

Instantly share code, notes, and snippets.

@juancarlospaco
Last active May 22, 2023 17:19
Show Gist options
  • Save juancarlospaco/94f98cff7f9accd8d618 to your computer and use it in GitHub Desktop.
Save juancarlospaco/94f98cff7f9accd8d618 to your computer and use it in GitHub Desktop.
Python3 Annotations as Static Typing, checks code blocks inputs and outputs enforcing Types via Decorator (change "log.critical" to "log.exception" to Force Fail).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import functools
import logging as log
def typecheck(f):
"""Decorator for Python3 annotations to type-check inputs and outputs."""
def __check_annotations(tipe):
_type, is_ok = None, isinstance(tipe, (type, tuple, type(None)))
if is_ok: # Annotations can be Type or Tuple or None
_type = tipe if isinstance(tipe, tuple) else tuple((tipe, ))
if None in _type: # if None on tuple replace with type(None)
_type = tuple([_ if _ is not None else type(_) for _ in _type])
return _type, is_ok
@functools.wraps(f) # wrap a function or method to Type Check it.
def decorated(*args, **kwargs):
msg = "Type check error: {0} must be {1} but is {2} on function {3}()."
notations, f_name = tuple(f.__annotations__.keys()), f.__code__.co_name
for i, name in enumerate(f.__code__.co_varnames):
if name not in notations:
continue # this arg name has no annotation then skip it.
_type, is_ok = __check_annotations(f.__annotations__.get(name))
if is_ok: # Force to tuple
if i < len(args) and not isinstance(args[i], _type):
log.critical(msg.format(repr(args[i])[:50], _type,
type(args[i]), f_name))
elif name in kwargs and not isinstance(kwargs[name], _type):
log.critical(msg.format(repr(kwargs[name])[:50], _type,
type(kwargs[name]), f_name))
out = f(*args, **kwargs)
_type, is_ok = __check_annotations(f.__annotations__.get("return"))
if is_ok and not isinstance(out, _type) and "return" in notations:
log.critical(msg.format(repr(out)[:50], _type, type(out), f_name))
return out # The output result of function or method.
return decorated # The decorated function or method.
if __name__ in "__main__":
@typecheck
def test_pass(a: int, b: list, c: tuple=(1, 2, 3)) -> float:
return 42.00
test_pass(1, [2, 3], (4, 5)) # this should pass ok.
@typecheck
def test_fail(a: int, b: list, x: "non-type", c: tuple=(1, 2, 3)) -> dict:
return "MUST Fail. Using Python3 Annotations as Static Typing Checks."
test_fail("This", "will", "fail", "but its ok") # this should fail.
@juancarlospaco
Copy link
Author

juan@z:~$ python3 recipe.py
CRITICAL:root:Type check error: 'This' must be (<class 'int'>,) but is <class 'str'> on function test_fail().
CRITICAL:root:Type check error: 'will' must be (<class 'list'>,) but is <class 'str'> on function test_fail().
CRITICAL:root:Type check error: 'but its ok' must be (<class 'tuple'>,) but is <class 'str'> on function test_fail().
CRITICAL:root:Type check error: 'MUST Fail. Using Python3 Annotations as Static Ty must be <class 'dict'> but is <class 'str'> on function test_fail().
juan@z:~$
  • You can change log.critical() to log.exception() or assert to Force Fail and TraceBack.
  • Annotations can be anything, if its type or tuple or None will be processed and checked.
  • [ ] and { } as annotations are ignored, *args and **kwargs without annotations are ignored.
  • Its not real Static Typing, but is as close as it can get on Duck Typing, This only works on Python3. 😏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment