Using Design by Contract in Python
I am looking to start using DBC on a large number of Python-based projects at work and am wondering what experiences others have had with it. So far my research turned up the following:
- http://www.python.org/dev/peps/pep-0316/ - PEP 316 that is supposed to standardize design by contract for Python which has been deferred. This PEP suggests using docstrings.
- http://www.wayforward.net/pycontract/ - Contracts for Python. This seems to be a complete, but unmaintained framework using docstrings.
- http://www.nongnu.org/pydbc/ - PyDBC which implements contracts using metaclasses. Also unmaintained for a few years.
My questions are: have you used DBC with Python for mature production code? How well did it work/was it worth the effort? Which tools would you recommend?
The PEP you found hasn't yet been accepted, so there isn't a standard or accepted way of doing this (yet -- you could always implement the PEP yourself!). However, there are a few different approaches, as you have found.
Probably the most light-weight is just to simply use Python decorators. There's a set of decorators for pre-/post-conditions in the Python Decorator Library that are quite straight-forward to use. Here's an example from that page:
>>> def in_ge20(inval):
... assert inval >= 20, 'Input value < 20'
...
>>> def out_lt30(retval, inval):
... assert retval < 30, 'Return value >= 30'
...
>>> @precondition(in_ge20)
... @postcondition(out_lt30)
... def inc(value):
... return value + 1
...
>>> inc(5)
Traceback (most recent call last):
...
AssertionError: Input value < 20
Now, you mention class invariants. These are a bit more difficult, but the way I would go about it is to define a callable to check the invariant, then have something like the post-condition decorator check that invariant at the end of every method call. As a first cut you could probably just use the postcondition decorator as-is.
In my experience design-by-contract is worth doing, even without language support. For methods that aren't overridden assertions, along with docstrings are sufficient for both pre- and postconditions. For methods that are overridden we split the method in two: a public method which check the pre- and post-conditions, and a protected method which provide the implementation, and may be overridden by subclasses. Here an example of the latter:
class Math:
def square_root(self, number)
"""
Calculate the square-root of C{number}
@precondition: C{number >= 0}
@postcondition: C{abs(result * result - number) < 0.01}
"""
assert number >= 0
result = self._square_root(number)
assert abs(result * result - number) < 0.01
return result
def _square_root(self, number):
"""
Abstract method for implementing L{square_root()}
"""
raise NotImplementedError()
I got the square root as a general example of design-by-contract from an episode on design-by-contract on software-engineering radio (http://www.se-radio.net/2007/03/episode-51-design-by-contract/). They also mentioned the need for language support because assertions weren't helpful in ensuring the Liskov-substitution-principle, though my example above aims to demonstrate otherwise. I should also mention the C++ pimpl (private implementation) idiom as a source of inspiration, though that has an entirely different purpose.
In my work, I recently refactored this kind of contract-checking into a larger class hierarchy (the contract was already documented, but not systematically tested). Existing unit-tests revealed that the contracts were violated multiple times. I can only conclude this should have been done a long time ago, and that unit-test coverage pays off even more once design-by-contract is applied. I expect anyone who tries out this combination of techniques to make the same observations.
Better tool-support may offer us even more power in the future, I welcome that.
I haven't used design by contract in python, so I can't answer to all your questions. However, I've spent some time looking at contracts library, whose latest version has been released recently, and it looks pretty nice.
There was some discussion about this library in reddit.
We wanted to use pre/post-conditions/invariants in our production code, but found that all current design-by-contract libraries lacked informative messages and proper inheritance.
Therefore we developed icontract. The error messages are automatically generated by re-traversing the decompiled code of the function and evaluating all the involved values:
import icontract
>>> class B:
... def __init__(self) -> None:
... self.x = 7
...
... def y(self) -> int:
... return 2
...
... def __repr__(self) -> str:
... return "instance of B"
...
>>> class A:
... def __init__(self)->None:
... self.b = B()
...
... def __repr__(self) -> str:
... return "instance of A"
...
>>> SOME_GLOBAL_VAR = 13
>>> @icontract.pre(lambda a: a.b.x + a.b.y() > SOME_GLOBAL_VAR)
... def some_func(a: A) -> None:
... pass
...
>>> an_a = A()
>>> some_func(an_a)
Traceback (most recent call last):
...
icontract.ViolationError:
Precondition violated: (a.b.x + a.b.y()) > SOME_GLOBAL_VAR:
SOME_GLOBAL_VAR was 13
a was instance of A
a.b was instance of B
a.b.x was 7
a.b.y() was 2
We found the library pretty useful both in the production (due to informative messages) and during the development (since it allows you to spot bugs early on).