Skip to content

hill-a/python-runtime-check

Repository files navigation

python-runtime-check

Build Status Build status CC-0 license Python Version Codacy Badge Codacy Badge

Information

Disclosure

I did not directly come up with this. This 'blackmagic' is inspired from a talk by David Beazley. It's quite fun watch if you have an hour to spare :)

Video:
The 'Fun' of Reinvention by David Beazley

As such, all the code in this repository is Creative Commons 0 (CC0)

What is this?

This is a python 3 only library, used for type checking and bound checking, enforced at runtime.

Now before I begin, yes I am aware of python's ducktyping. However some situations have arisen in the past, where I needed to garanty that my inputs where sanitized. For example, a list a = [] that should contain only number, if a.append('') occure, it will lead to undesired side effects. Or even with ParseArgument, if you need a number between 0 and 1 for probabilistic usage.

Also, I find it sad that the annotations of types in methods a: int is not an enforced restriction at runtime (it is however very useful with MyPy or some IDEs).

Hence this library, where annotations can be enforced if present, types checked, or bounds of numbers checked.

When should I use this?

Preferably, in situations where you REALLY need to. This is not designed to run on every function (you can't neglect the overhead cost), as such here are some (rather poor) examples of when to use this:

def print_information(info):
    print("here is the information:" + info)

Here, we would prefere info to be of type string.

def print_informations(infos):
    for info in infos:
        print("here some of the information:" + info)

Here, we would prefere infos to be of type list of string.

def dice_roll(proba):
    roll = random.random()
    if roll <= proba:
        print("success!")
        return True
    else:
        print("failure!")
        return False

Here, we would prefere proba to be of type float, but also bound between [0, 1].

Installation

This library will require NumPy and python 3.5 or higher:

pip install numpy

You can install this using:

pip install git+https://github.com/hill-a/python-runtime-check

Usage

Type checking

You can enforce the python type annotations at call:

 # a can either be a int, or a float
@check_type_at_run
def hello(a: Union[int, float]):
    pass

 # a has to be an int
@check_type_at_run
def hello(a: int):
    pass

 # c can be either None, or a list composed of anything
 # the return value can either be an int, or a string
@check_type_at_run
def hello(a: int, b: str, c: Optional[List[Any]] = []) -> Union[int, str]:
    if c is None:
        return b
    else: 
        return a

Or check them during execution:

TypeChecker[int, float](0) # here the comma seperated types are replaced with typing.Union internaly
TypeChecker[Tuple[int, float, str]]((0, 1.0, 'a'))
TypeChecker[Optional[Dict[str, int]]](None)
TypeChecker[Optional[Dict[str, int]]]({'a':1, 'b':2})
TypeChecker.iterable("hello")
TypeChecker.numpy_array(numpy.arange(10))

Should you need to check all the elements of a list, dict, set, tuple or sequence when type checking, set this flag runtime_check.check_type.DEEP = True.

Here are some useful types commonly used in python:

  • numpy.ndarray
  • torch.FloatTensor
  • torch.tensor._TensorBase
  • collections.Iterable
  • type(None)
  • typing.Optional[int]
  • typing.Union[int, float]
  • typing.List, typing.Dict, typing.Set, typing.Tuple, typing.Iterable
  • typing.Any, typing.TypeVar, typing.AnyStr

python typing annotations here.

Bounds checking

You can check annotated bounds at call (however the notation is not very readable):

 # a must be between ]-inf, 1] or [0, 1] or [2, +inf[
@check_bound_at_run
def hello(a: [(float('-inf'), -1), (0, 1), (2, float('+inf'))]):
    pass

 # a must be between [0,1]
@check_bound_at_run
def hello(a: (0, 1)):
    pass

 # a must be between ]0, +inf[
 # b must be between [0, 1]
 # return must be between [0, 100]
@check_bound_at_run
def hello(a: (0, float('+inf'), (False, True)), b: (0, 1)) -> (0, 100):
    if a < (b * 100):
        return b * 100
    else:
        return min(a, 100)

Or check them during execution:

BoundChecker[(0, 1), (2, 4)](0.5)         # [0, 1] or [2, 4]
BoundChecker[(0, 100, (True, False))](20) # [0, 100[
BoundChecker.positive(100)                # [0, +inf[

the tuple defining the bounds are:
(Lower_bound, Upper_bound, (Include_lower_bound, Include_upper_bound))
or:
(Lower_bound, Upper_bound)
The bounds must be numbers (float or int), if the Include_lower_bound and Include_upper_bound are not defined, they default to True.

You may use lists of bounds to define discontinuous bounds

Chained checking

You may also combine the previous execution checks, to validate a variable with annotations:

@enforce_annotations
def hello(a: [BoundChecker[(0, 1)], TypeChecker[int, float]]):
    pass

@enforce_annotations
def hello(a: TypeChecker[str]):
    pass

@enforce_annotations
def hello(a: [BoundChecker[(0, 1)], TypeChecker[int, float]]) -> [BoundChecker[(0, 1, (False, True))], TypeChecker[float]]:
    return 0.2

About

simple type and bound enforcer code

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published