Typesafety is a tool for Python (3.2 or newer) that - using annotations - checks if the arguments for function calls are valid. For example, consider the following piece of code:
def sign(x):
if not isinstance(x, int):
raise TypeError('Invalid type {!r}, expected int'.format(x))
if x < 0:
return -1
if x > 0:
return 1
return 0
The manual type check can become cumbersome really fast, especially when there are multiple arguments, complex checks, etc. Wouldn't it be cool if for internal (i.e. not API) code the checks could be done more concisely? If you could write:
def sign(x: int):
# ...
Typesafety is meant to be used during testing. True, the checker can be turned on in production code but the performance slowdown can make this undesirable.
Typesafety comes with builtin plugins for two popular testing frameworks, nosetests and pytest (our preferred tool at Balabit is nosetests), and using it is very simple.
For nose:
$ nosetests --enable-typesafety mymodule
And similarly for pytest:
$ py.test --enable-typesafety mymodule
And voila! Type checking is enabled for the module mymodule
during tests.
The typesafety tool can also be enabled "manually," using the
typesafety.activate
function. Consider the module testmod
:
def my_function(x: int) -> int:
return x + 1
Before importing testmod
, we need to enable typesafety:
import typesafety
import imp
import testmod
testmod.my_function(1.0) # No error, since typesafety is not enabled
# Note that the filter_func optional argument can be used to filter
# which modules will be type checked.
typesafety.activate(filter_func=lambda name: name.startswith('testmod.'))
testmod = imp.reload(testmod)
testmod.my_function(1.0) # Will throw a TypesafetyError
NOTE: We use the exception TypesafetyError
instead of the more
appropriate, built-in TypeError
since raising a TypeError
would cause
tests asserting for TypeError
to pass if the arguments are wrong.
If you are using typesafety with another lib that uses annotations, it might cause some interference. In this case, you should be able to disable typesafety checking for certain functions. But how to do this?
The preferred way is simply to mark the function for skipping:
def dont_check(x: (int, 'This annotation has another meaning')) -> (float, 'As does this'):
return 'Definitely not a float'
dont_check.typesafety_skip = True
When the typesafety_skip
attribute is set for a function, it will not check
the calls to that function.
A function with argument or return value annotations will be used to implement the type safety check mechanism. For further information on how annotations work, see the Python documentation.
The simplest type safety check is when a singular type is specified for an argument or return value:
def my_function(x: int) -> float:
return float(x) + 1.0
my_function(1) # Will return 2.0
my_function(1.0) # Will throw a TypesafetyError
In this case on each call the type safety checker will validate that
the argument is an int
and the return value is a float
.
Some conditions cannot be checked by isinstance
. If the parameter needs
to be a callable object (i.e. function, object with __call__
implemented,
etc.) we can annotate the argument or return value with a callable:
def decorator(func: callable) -> callable:
# ...
return res
@decorator
def my_function(x):
pass
decorator(1) # Will throw a TypesafetyError
If a tuple is specified in the annotation, then at least one of the specified conditions must apply to the argument.
def multiple_argument_types(number: (int, float)) -> (int, float):
return number + 1
multiple_argument_types(1) # Will return 2
multiple_argument_types(1.0) # Will return 2.0
multiple_argument_types('string') # Will throw a TypesafetyError
To avoid having to write parameter documentation manually, the
typesafety.sphinxautodoc
Sphinx extension is provided. It will
automatically add the typesafety annotations to the signatures that
Sphinx autodoc puts into the documentation.
In your Sphinx config file, simply add typesafety.sphinxautodoc
to the
extension list:
extensions.append('typesafety.sphinxautodoc')
Custom decorator functions often work like the following:
from functools import wraps
def some_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Do some additional stuff, and then...
return func(*args, **kwargs)
return wrapper
@some_decorator
def my_annotated_function(x: int):
pass
This way the documentation for my_annotated_function
will use the
signature of the decorated function, ie. it will be just *args,
**kwargs
which is not very helpful. Sadly, there is no out-of-the-box
solution for this problem, however, if the decorator is extended with
setting the decorated_function
attribute of the wrapper function it
returns, then typesafety.sphinxautodoc
will use that attribute to
read the signature from:
def some_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Do some additional stuff, and then...
return func(*args, **kwargs)
wrapper.decorated_function = func
return wrapper
Using the above version of @some_decorator
will enable
typesafety.sphinxautodoc
to generate the proper signature
documentation for my_annotated_function()
, ie. (x: int)
.