PyContracts is a Python package that allows to declare constraints on function parameters and return values. It supports a basic type system, variables binding, arithmetic constraints, and has several specialized contracts (notably for Numpy arrays).
Status: PyContracts is very well tested and documented. The last release is version 1.2.0 (November 2011). See changelog.
Why: The purpose of PyContracts is not to turn Python into a statically-typed language (albeit you can be as strict as you wish), but, rather, to avoid the time-consuming and obfuscating checking of various preconditions. In fact, more than the type constraints, I found useful the ability to impose value and size constraints. For example, “I need a list of at least 3 positive numbers” can be expressed as list[>=3](number, >0)). If you find that PyContracts is overkill for you, you might want to try a simpler alternative, such as typecheck. If you find that PyContracts is not enough for you, you probably want to be using Haskell instead of Python.
Contracts can be specified in three ways:
Using annotations (for Python 3) —this is perhaps the most intuitive way:
@contract
def my_function(a : 'int,>0', b : 'list[N],N>0') -> 'list[N]':
# Requires b to be a nonempty list, and the return
# value to have the same length.
...
Using :type: and :rtype: tags in docstrings. In this way, they will be included in your Sphinx documentation:
@contract
def my_function(a, b):
""" Function description.
:type a: int,>0
:type b: list[N],N>0
:rtype: list[N]
"""
...
Using arguments to the decorators; the least intrusive way:
@contract(a='int,>0', b='list[N],N>0', returns='list[N]')
def my_function(a, b):
...
Moreover, there are utility functions for manual checking of values:
check('array[HxWx3](uint8),H>10,W>10', image)
as well as hooks to extend PyContracts with new contracts types:
new_contract('valid_name', lambda s: isinstance(s, str) and len(s)>0)
check('dict(int: (valid_name, int))', employees)
Support: use the GitHub issue tracker or email me.
Documentation index
Install PyContracts using:
$ pip install PyContracts
or from GitHub:
$ git clone git://github.com:AndreaCensi/contracts.git
$ cd contracts
$ python setup.py develop
$ nosetests -w src # run the extensive test suite
The beautiful library pyparsing is required.
The contracts are specified using the type clause of RST-style docstrings (now accepted as standard in the python libraries); or, they can be passed explicitly to the @contract decorator. In this example, PyContracts is smart enough to check that the two parameters a and b are matrices of compatible dimensions. Then, it checks that the result value is of compatible dimensions as well.
import numpy
from contracts import contract
@contract
def matrix_multiply(a, b):
''' Multiplies two matrices together.
:param a: The first matrix. Must be a 2D array.
:type a: array[MxN],M>0,N>0
:param b: The second matrix. Must be of compatible dimensions.
:type b: array[NxP],P>0
:rtype: array[MxP]
'''
return numpy.dot(a, b)
PyContracts can come in handy when you have operations that could be one-liners if you are sure of the types of the parameters, but doing all the checking adds to the complexity of the code.
In the next example we check that:
The two lists have elements of the same type (indicated by the variable x);
The returned list has the correct size (the sum of the two lengths).
@contract( a='list[ M ](type(x))',
b='list[ N ](type(x))',
returns='list[M+N](type(x))')
def my_cat_equal(a, b):
''' Concatenate two lists together. '''
return a + b
The philosophy is to make the simple cases easy, and the difficult possible, while still retaining readability.
For example, we can either ask for a simple list, or specify more about it using the additional clauses.
| Contract expression | Meaning |
|---|---|
| list | An instance of list. |
| list[2] | A list of two elements. |
| list(int) | A list of integers. |
| list(number) | A list of numbers. |
| list[3](number) | A list of exactly three numbers. |
| list[>=3](number) | A list of at least three numbers. |
| list[>=3](number, >0) | A list of at least three numbers, greater than 0. |
PyContracts supports the use of variables. There are two kinds of variables: lower-case letters (a, b, ...) are general-purpose variables, while upper-case letters (A, B, ...) are constrained to bind to integer types; they are meant to represent sizes and shapes. Moreover, PyContracts can do arithmetic and comparisons.
| Contract expression | Meaning |
|---|---|
| tuple(list[N], list[N]) | A tuple with two lists of the same length. |
| tuple(list[N], list[M]), N<M | A tuple with two lists, the first one being shorter. |
| list[>0](type(x)) | A non-empty list containing elements of all the same type. |
| tuple(list(type(x)), list(type(x))) | A tuple with two lists containing objects of the same type. |
For the complete reference to the available contract expressions, see Language reference.
This is a discussion of the PyContracts API.
The decorator contracts() is considered the main way to define constraints. It is quite flexible, and it is smart enough to support functions with variable number of arguments and keyword arguments.
There are three ways to specify the contracts. In order of precedence:
PyContracts will try these options in order. Note that, in any case, only one of these options are chosen. For example, you cannot use both annotations and docstring for the same function: if annotations are found, the docstring is not considered.
contract() accepts a list of keyword arguments. Each keyword should correspond to one function argument, plus the special name returns is reserved for describing the return value. An example use would be:
from contracts import contract
@contract(a='int,>0', b='list[N],N>0', returns='list[N]')
def my_function(a, b):
...
The values can be either:
Strings using PyContracts‘ DSL language (see Language reference)
Python types — in this case PyContracts will do a simple isinstance() check. This is slightly more clear if the contract is simple:
@contract(a=int, b=float, returns=float)
def my_function(a, b):
return a + b
The same rules apply. In this case the syntax would look like this:
from contracts import contract
@contract
def my_function(a:'int,>0', b:'list[N],N>0') -> 'list[N]':
...
The Python standard library seems to have standardized on the :param:, :type:, :return:, :rtype: tags to document functions, and tools like Sphinx can interpret those tags to produce pretty documentation.
PyContracts can read contracts declared using the :type: and :rtype: tags. In this way, your function becomes automatically more robust and better documented.
Here is an example use:
from contracts import contract
@contract
def my_function(a, b):
""" Function description.
:param a: first number
:type a: int,>0
:param b: description of b
:type b: list[N],N>0
:return: a list
:rtype: list[N] """
...
Note
By convention, those annotations must be parsable as reStructuredText. If the contract string has special RST characters in it, like *, you can include it in double ticks. PyContracts will remove the double ticks before interpreting the string.
For example, the two annotations in this docstring are equivalent for PyContracts, but the latter is better for Sphinx:
""" My function
:param a: First parameter
:type a: list(tuple(str,*))
:param b: First parameter
:type b: ``list(tuple(str,*))``
"""
The public interface of PyContracts contains a function decorate() that can be used to decorate functions after they are defined. For example:
from contracts import decorate
def f(a):
a.pop()
return a
f2 = decorate(f, a='list[N],N>1', returns='list[N-1]')
f2([]) # raises exception before pop() is executed
In addition to the @contract() decorator, PyContracts offers an interface for checking expressions explicitly.
The check() function allows you to check that some value respect a given contract.
from contracts import check
check('tuple(int,int)', (2, 2) )
check() might raise:
Both exceptions are instances of ContractException. Usually you want to use check() as a better assert(), so you don’t want to catch those exceptions. The error message should be descriptive enough.
For example, the message generated by
check('tuple(int, tuple(*,str))', (0, (1, 2)))
is
contracts.ContractNotRespected: Expected a string, got 'int'.
context: checking: str for value: Instance of int: 2
context: checking: tuple(*,str) for value: Instance of tuple: (1, 2)
context: checking: tuple(int,tuple(*,str)) for value: Instance of tuple: (0, (1, 2))
The message shows the recursive matches of expressions, and should be enough to diagnose the problem. Nevertheless, you can add a third argument to specify a message that will be included in the error, if one is thrown. For example:
score = (2, None)
check('tuple(int,int)', score, 'Player score must be a tuple of 2 int.' )
In the case you are using variables, the matching “context” is shown. The context is the set of variables which have already bound to values.
For example, suppose you want a tuple with two lists of equal length. This can be expressed as:
value = ([0,1],[1,2,3]) # invalid
check('tuple(list[N], list[N])', value)
What happens behind the scenes is that PyContracts first matches the first list, binds the variable N to the length of that list (=2), then checks that list[N] describes the second list. Now, however, N has already been bound, and the matching fails. The error you would see is:
contracts.ContractNotRespected: Expected that 'N' = 2, got 3.
checking: N (N=2) for value: Instance of int: 3
checking: list[N] (N=2) for value: Instance of list: [1, 2, 3]
checking: tuple(list[N],list[N]) for value: Instance of tuple: ([0, 1], [1, 2, 3])
PyContracts tries hard to give you a readable message even with complicated contracts. For example, consider the complicated expression:
check('int|str|tuple(str,int)', ('a', 'b'))
Because there is an OR (|), PyContracts will have to explain to you why none of the three OR clauses was satisfied.
contracts.interface.ContractNotRespected: Could not satisfy any of the 3 clauses in int|str|tuple(str,int).
---- Clause #1: int
| Expected type 'int', got 'tuple'.
| checking: int for value: Instance of tuple: ('a', 'b')
---- Clause #2: str
| Expected a string, got 'tuple'.
| checking: str for value: Instance of tuple: ('a', 'b')
---- Clause #3: tuple(str,int)
| Expected type 'int', got 'str'.
| checking: int for value: Instance of str: 'b'
| checking: tuple(str,int) for value: Instance of tuple: ('a', 'b')
------- (end clauses) -------
checking: int|str|tuple(str,int) for value: Instance of tuple: ('a', 'b')
The results of parsing are cached internally, there is no performance penalty in using check() often.
If you wish, you can also separate the parsing and checking phases. The function parse() will return a Contract object, which in turn has a Contract.check() function you can call.
from contracts import parse
contract = parse('>0') # May raise ContractSyntaxError
contract.check(2) # May raise ContractNotRespected
The most interesting uses of PyContracts are when it is used to check that multiple objects respect mutual constraints.
You can use check_multiple() to check that a series of values respect a series of contracts, that might reference each other’s variables. The argument to check_multiple() must be a list of tuples with two elements (contract, value).
In the next example, we are defining a table. We want data to be a list of lists with equal length, and that row_labels and col_labels have the expected length, coherent with data. PyContracts allows you to do all this checks with a remarkably clear, concise, and readable notation.
from contracts import check_multiple
data = [[1,2,3],
[4,5,6]]
row_labels = ['first season', 'second season']
col_labels = ['Team1', 'Team2', 'Team3']
check_multiple([('list[C](str),C>0', col_labels),
('list[R](str),R>0', row_labels),
('list[R](list[C](numbers))', data) ],
'I expect col_labels, row_labels, and data to '
'have coherent dimensions.')
If you want to examine the contract object, use Contract.__repr__() for eval()uable expression, or Contract.__str__() to get back a representation using PyContracts‘ language.
from contracts import parse
contract = parse(' list[N](int), N > 0')
print("%s" % contract) # => 'list[N](int),N>0'
print("%r" % contract)
# => And([List(BindVariable('N',int),CheckType(int)),
# CheckOrder(VariableRef('N'),'>',SimpleRValue(0))])
The latter is useful if you are hacking PyContracts and you want to check in detail what is going on.
If you are using binding expression, you can examine what variables were bound using the return values. The functions check() and Contract.check() will return a Context object.
context = check('list[N](str), N>0', ['a','b','c'])
print('N' in context) # => True
print(context['N']) # => 3
Defines a new contract type. The second parameter can be either a string or a callable function.
If it is a string, it is interpreted as contract expression; the given identifier will become an alias for that expression.
If it is a callable, it must accept one parameter, and either:
If ValueError is raised, its message is used in the error.
This function returns a Contract object. It might be useful to check right away if the declaration is what you meant, using Contract.check() and Contract.fail().
For example, suppose that you are writing a graphical application and that many of your functions need arguments representing colors. It might be a good idea to declare once and for all what is a color, and then reuse that definition. For example:
color = new_contract('color', 'list[3](number,>=0,<=1)')
# Make sure we got it right
color.check([0,0,0])
color.check([0,0,1])
color.fail([0,0])
color.fail([0,0,2])
# Now use ``color`` in other contracts.
@contract
def fill_area(inside, border):
""" Fill the area inside the current figure.
:type border: color
:type inside: color """
...
@contract
def fill_gradient(colors):
""" Use a gradient to fill the area.
:type colors: list[>=2](color) """
...
You can use contract_expression to add a contract expression to your PyParsing-based grammar:
from contracts import contract_expression
from pyparsing import Literal, Optional
my_grammar = ... + Optional(Literal('respects') + contract_expression)
This section describes PyContracts‘ language for specifying contracts.
The simplest kind of contract consists in declaring the type of a value. These are the supported primitive types:
dict, list, tuple, float, int, number, array, bool, None
Moreover, there are two kinds of wild-cards:
PyContracts allows to use variables to bind to sub-expressions and reuse later (it is simpler than it sounds).
There are two kinds of variables. The first kind is denoted by an uppercase letter, and it is constrained to bind to integer values.
A B C D E F G H I J K L M N O P Q R S T U W V X Y Z
Lower-case letters denote general-purpose variables that can bind to any type:
a b c d e f g h i j k l m n o p q r s t u w v x y z
Note
The reason for having specialized variables for integers is to encourage a writing style in which uppercase letters denote lengths, shapes, etc. Moreover, an error will be thrown if they do not bind to integers, which helps in catching mistakes.
Two logical operators are supported:
Expressed with a comma (,).
For example, the expression
check("contract1,contract2", value)
is roughly equivalent to
check("contract1", value) and check("contract2", value)
except that variables in the contracts are evaluated in the same context.
Expressed with a pipe symbol (|).
The semantic is that the first matching expression is chosen.
The expression:
check("contract1|contract2", value)
is roughly equivalent to:
if value respects "contract1":
return True
elif value respects "contract2":
return True
else:
return False
The AND has precedence over OR. For example, the expression
a,b|c
is evaluated the same as
(a, b) | c
| Contract expression | Meaning |
|---|---|
| None|int | Either None or an integer. |
This section must still be written.
This section must still be written.
This section must still be written.
You can specify that the value must be a list, and specify optional constraints for its length and for its elements.
The general syntax is:
list
list[length_contract]
list(elements_contract)
list[length_contract](elements_contract)
| Contract expression | Meaning |
|---|---|
| list | An instance of list. |
| list[2] | A list of two elements. |
| list(int) | A list of integers. |
| list(number) | A list of numbers. |
| list[3](number) | A list of exactly three numbers. |
| list[>=3](number) | A list of at least three numbers. |
| list[>=3](number, >0) | A list of at least three numbers, greater than 0. |
| tuple(list[N], list[N]) | A tuple of two lists of the same length. |
The contract seq has the same syntax as list but matches any sequence.
seq
seq[length_contract]
seq(elements_contract)
seq[length_contract](elements_contract)
You can either specify a length (with square brackets), or specify a contract for each element:
tuple
tuple[length]
tuple(element1, ..., elementN)
In the latter case, you are also specifying implicitly the number of elements.
Note
The syntax for tuples is somewhat a special case. While list(int,>0) signifies a list of positive integers (or empty list), tuple(int, >0) means a tuple with exactly 2 elements, the first of which should be an integer, and the second must be positive (but not necessarily integer.)
| Contract expression | Meaning |
|---|---|
| tuple | A tuple. |
| tuple[2] | A tuple with two elements. |
| tuple(*,*) | A tuple with two elements. |
| tuple(int) | A tuple with one integer element. |
| tuple(int, int) | A tuple with two integer elements. |
| tuple[>=2] | A tuple with at least two elements. |
For dictionary, you can specify a length contract, as well as a contract for its keys and values:
dict
dict[length_contract]
dict(key_contract: value_contract)
dict[length_contract](key_contract: value_contract)
| Contract expression | Meaning |
|---|---|
| dict | Any dictionary. |
| dict[2] | A dictionary with two elements. |
| dict(*: *) | Any dictionary. |
| dict(*: int) | A dictionary whose values are integers. |
| dict(str: *) | A dictionary whose keys are strings |
| dict(str: tuple(type(x),type(y)) ), x!=y | A dictionary whose keys are strings, and whose values are tuples with two elements, of two different types x and y. |
The contract map has the same syntax as dict but matches any mapping.
map
map[length_contract]
map(key_contract: value_contract)
map[length_contract](key_contract: value_contract)
The support for Numpy arrays was one of the motivations for me to develop PyContracts. Numpy is one of the best and most useful Python packages around. It supports the ndarray datatype which allows for operations with tensors of arbitrary dimensions.
All of that is very powerful, but it might be a bit confusing, especially because Numpy tends to be very liberal when it gets to operations between arrays. For example, the operation A * B, if A is a 2D matrix, is well defined when B is a scalar, a vector, or a matrix. Sometimes you want to be certain of your assumptions, otherwise you risk of ignoring powerful bugs.
Note
Researchers tend to be a bit paranoid, because often you don’t know what to expect out of your algorithms. A stupid bug can lead to an exciting (false) discovery!
So PyContracts offers several shortcuts for Numpy arrays, and the error messages tend to be more descriptive.
The general syntax looks like this:
array
array[shape_contract]
array(numpy_contract)
array[shape_contract](numpy_contract)
A shape contract is specified using a special syntax of the kind:
dimension1 x dimension2 x dimension3
dimension1 x dimension2 x dimension3 x ...
The ellipsis is part of the syntax and specifies that more dimensions are allowed.
Each dimension can be specified as a number, with variables, or as a contract.
| Contract expression | Meaning |
|---|---|
| array | Any array. |
| array[3] | A 1D array with 3 elements (shape=(3,)). |
| array[3x2] | A 2D array with 3 rows and 2 columns (shape=(3,2)). |
| array[3 x ...] | A nD array with 3 rows and arbitrary other dimensions. |
| array[3xN], N>=2 | A 2D array with 3 rows and at least 2 columns. |
| array[NxN], N>0 | A square matrix. |
| array[NxNx...], N>0 | A tensor with the first two dimensions of equal size. |
| list(array[NxN]), N>0 | A list of square matrices of the same size. |
A numpy contract is special and implemented separately from the other PyContracts contract. Right now, we support:
These specify the datatype of the array. Available:
uint8 uint16 uint32 uint64 int8 int16 int32 int64 float32 float64
These have the semantics that all elements must satisfy the contract. They appear similar to the normal PyContracts comparison:
>0 <0 <=1 =0
| Contract expression | Meaning |
|---|---|
| array | Any array. |
| array(>=0) | An array with nonnegative elements. |
| array(int8,>=0) | An array with nonnegative integer elements. |
| array[NxN](float,>=0,<=1) | A square matrix with float elements in the interval [0,1]. |
| array[NxN](int,(0|1)) | A square matrix with each element equal either to 0 or 1 (this could model a directed graph). |
Here is an example that first uses new_contract() to specify a domain-specific array type (rgb, rgba), then uses PyContracts to be sure that the two given images and the result values have coherent dimensions. Notice how the contracts serve very well as documentation.
from contracts import contract, new_contract
new_contract('rgb', 'array[HxWx3](uint8),H>0,W>0')
new_contract('rgba', 'array[HxWx4](uint8),H>0,W>0')
@contract
def blend_images(image1, image2):
'''
Blends two images together.
:param image1: The first image to blend.
:type image1: rgb,array[HxWx*]
:param image2: The second image to blend. Can have an alpha layer.
:type image2: (rgb|rgba),array[HxWx*]
:return: The blended image.
:rtype: rgb,array[HxWx3]
'''
...
If you want to have a function that accepts a list of images, you would write:
@contract
def blend_images(images):
'''
Blends a series of images together.
:type images: list[N](rgb,array[HxWx*]), N>=2
:rtype: rgb,array[HxWx3]
'''
...
PyContracts has a special construct $(...) used to separate the context of variables. The semantics is that the variables assigned inside $(...) are not propagated outside.
| Contract expression | Meaning |
|---|---|
| list(tuple(type(x),type(y)),x!=y) | A list whose elements are tuples with two elements, of two different types x and y (x and y are common among the list elements ). |
| list( $( tuple(type(x),type(y)),x!=y) ) | A list whose elements are tuples with two elements, of two different types x and y. (x and y are not joined among the list elements ). |
This section provides a reference to PyContracts‘ public interface.
Decorator for adding contracts to functions.
It is smart enough to support functions with variable number of arguments and keyword arguments.
There are three ways to specify the contracts. In order of precedence:
As arguments to this decorator. For example:
@contract(a='int,>0',b='list[N],N>0',returns='list[N]')
def my_function(a, b):
# ...
pass
As annotations (supported only in Python 3):
@contract
def my_function(a:'int,>0', b:'list[N],N>0') -> 'list[N]':
# ...
pass
Using :type: and :rtype: tags in the function’s docstring:
@contract
def my_function(a, b):
""" Function description.
:type a: int,>0
:type b: list[N],N>0
:rtype: list[N]
"""
pass
Signature and docstrings: The signature of the decorated function is conserved. By default, the docstring is modified by adding :type: and :rtype: definitions. To avoid that, pass modify_docstring=False as a parameter.
Contracts evaluation: Note that all contracts for the arguments and the return values are evaluated in the same context. This make it possible to use common variables in the contract expression. For example, in the example above, the return value is constrained to be a list of the same length (N) as the parameter b.
Using docstrings Note that, by convention, those annotations must be parseable as RestructuredText. This is relevant if you are using Sphinx. If the contract string has special RST characters in it, like *, you can include it in double ticks. PyContracts will remove the double ticks before interpreting the string.
For example, the two annotations in this docstring are equivalent for PyContracts, but the latter is better for Sphinx:
""" My function
:param a: First parameter
:type a: list(tuple(str,*))
:param b: First parameter
:type b: ``list(tuple(str,*))``
"""
| Raise : | ContractException, if arguments are not coherent |
|---|---|
| Raise : | ContractSyntaxError |
An explicit way to decorate a given function. The decorator decorate() calls this function internally.
The base class for the exceptions thrown by this module.
Exception thrown when there is a syntax error in the contracts.
Exception thrown when a value does not respect a contract.
Checks that object satisfies the contract described by contract.
| Parameters: |
|
|---|
Checks multiple couples of (contract, value) in the same context.
This means that the variables in each contract are shared with the others.
| Parameters: |
|
|---|
spec can be either a Contract, a type, or a contract string. In the latter case, the usual parsing takes place
Defines a new contract type. Used both as a decorator and as a function.
1) Use as a function. The first parameter must be a string. The second parameter can be either a string or a callable function.
new_contract('new_contract_name', 'list[N]')
new_contract('new_contract_name', lambda x: isinstance(x, list) )
If it is a string, it is interpreted as contract expression; the given identifier will become an alias for that expression.
If it is a callable, it must accept one parameter, and either:
If ValueError is raised, its message is used in the error.
2) Use as a decorator.
Or, it can be used as a decorator (without arguments). The function name is used as the identifier.
@new_contract
def new_contract_name(x):
return isinstance(x, list)
This function returns a Contract object. It might be useful to check right away if the declaration is what you meant, using Contract.check() and Contract.fail().
| Parameters: |
|
|---|---|
| Returns: | The equivalent contract – might be useful for debugging. |
| Return type: | Contract |
Checks that the value satisfies this contract.
| Raise : | ContractNotRespected |
|---|
Checks that the value does not respect this contract. Raises an exception if it does.
| Raise : | ValueError |
|---|
Returns a string representation of a contract that can be evaluated by Python’s eval().
It must hold that: eval(contract.__repr__()) == contract. This is checked in the unit-tests.
Example:
>>> from contracts import parse
>>> contract = parse('list[N]')
>>> contract.__repr__()
"List(BindVariable('N',int),None)"
All the symbols you need to eval() the expression are in contracts.library.
>>> from contracts.library import *
>>> contract == eval("%r"%contract)
True
Returns a string representation of a contract that can be reparsed by contracts.parse().
It must hold that: parse(str(contract)) == contract. This is checked in the unit-tests.
Example:
>>> from contracts import parse
>>> spec = 'list[N]'
>>> contract = parse(spec)
>>> contract
List(BindVariable('N',int),None)
>>> str(contract) == spec
True
The expressions generated by Contract.__str__() will be exactly the same as what was parsed (this is checked in the unittests as well) if and only if the expression is “minimal”. If it isn’t (there is whitespace or redundant symbols), the returned expression will be an equivalent minimal one.
Example with extra parenthesis and whitespace:
>>> from contracts import parse
>>> verbose_spec = 'list[((N))]( int, > 0)'
>>> contract = parse(verbose_spec)
>>> str(contract)
'list[N](int,>0)'
Example that removes extra parentheses around arithmetic operators:
>>> verbose_spec = '=1+(1*2)+(2+4)'
>>> str(parse(verbose_spec))
'=1+1*2+2+4'
This is an example with logical operators precedence. The AND operator , (comma) has more precedence than the OR (|).
>>> verbose_spec = '(a|(b,c)),e'
>>> str(parse(verbose_spec))
'(a|b,c),e'
Not that only the outer parenthesis is kept as it is the only one needed.