54

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:

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?

ipartola
  • 1,612
  • 3
  • 15
  • 25
  • Note that you can just inherit from TestCase, and include unit tests in any class. – Marcin Dec 19 '11 at 15:25
  • 4
    Right, but DBC is a bit different in that it will run checks in production and on all data inputs. From what I understand Unit Tests are runtime asserts with a pre-defined data set, whereas DBC is a level-above asserts with all input. Specifically, I think it makes sense to use DBC in my case since a lot of the code is really state-heavy and often has to get state from an external DB with a frequently changing schema and fairly complex relationships which are very messy to mock up. – ipartola Dec 19 '11 at 15:35
  • Design by contract is where you explicitly specify the specification to which each piece of code conforms. You do not have to test it at runtime in full. Unit tests can be that specification just as much as anything else. TDD is a different way of using unit tests, in that case to model an expected set of behaviours. – Marcin Dec 19 '11 at 16:22
  • 1
    I understand that. In my case TDD is a bit more messy: pulling data from one, sometimes two external databases where large amounts of data are related in unexpected ways and cannot be easily mocked up. It seems to be that DBC might be a better fit, where having to worry about mocking up the data is no longer a concern. – ipartola Dec 19 '11 at 16:30
  • Everyone should probably be doing elements of both. – Marcin Dec 19 '11 at 16:43
  • 3
    Oh, absolutely! One is not a substitute for the other. I am just trying to figure out where to direct our efforts next, and it seems that DBC will give the bigger ROI at this point. Of course unit tests and DBC do not exclude each other and would be effective together. – ipartola Dec 19 '11 at 17:07

5 Answers5

22

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.

snim2
  • 4,004
  • 27
  • 44
  • Thank you for the answer. I understand the way that this could be used to implement DBC in Python. What I was wondering is whether anyone already had success with any of the libraries I mentioned, or any other libraries. The question is less of "how do I implement DBC?" and more of a "should I bother implementing DBC?". – ipartola Jan 24 '12 at 18:55
  • @ipartola since PEP 316 has been "deferred" it is not actively being worked on but has not been rejected. So, if you want to take it forward, making some improvements on PyContract would probably be a good way forward. I think you can assume work on PyContract has stalled for now. So, the answer to "should I bother implementing DBC" is "yes" in my opinion, it would be a very useful addition, but that's probably subjective as DBC is not yet widely used in the Python ecosystem. – snim2 Jan 24 '12 at 22:22
  • 1
    Also the Covenant library (https://bitbucket.org/kisielk/covenant/) from the link that @jcollado mentioned has invariants. – snim2 Jan 24 '12 at 22:37
  • If the sample code complains about "NameError: name 'precondition' is not defined", what am I missing? Thanks! – Jie Oct 08 '20 at 07:06
  • This was a while ago @Jie but I think it relied on importing some code from the [Python Decorator Library](https://wiki.python.org/moin/PythonDecoratorLibrary#Pre-.2BAC8-Post-Conditions) that was mentioned in the answer. This was Python 2 though, and I haven't looked at it in some time, so you may need to tweak the code a little! – snim2 Oct 08 '20 at 11:06
15

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.

  • Great anecdote that actually answers the question. – doughgle Nov 06 '13 at 09:38
  • Correct me if I'm wrong, but this doesn't seem to do well with class invariants. You could include them in each pre- and postcondition, but that will return false positives when a method (1) breaks the invariant (2) calls another method which checks the invariant (3) restores the invariant (which is allowed). You could adopt the discipline of never calling the public methods yourself, only calling the private helpers, but then the pre- and post-conditions won't be checked for those calls. – johncip Apr 24 '14 at 02:28
  • It also requires you to override the public method if an overriding private implementation updates the pre- or postconditions, but I suppose in practice that's not a huge deal, and to be fair I don't know that the other mentioned implementations handle these issues any better. – johncip Apr 24 '14 at 02:37
  • No, my post was only concerned with pre- and postconditions. I have no experience to offer on smart tricks for enforcing class invariants in Python. It is not hard to come up with ideas for how to do it, though. – Robert Jørgensgaard Engdahl Apr 24 '14 at 10:56
9

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).

marko.ristin
  • 643
  • 8
  • 6
  • 1
    Two years later, this seems the most interesting package, both as actively maintained, and providing most features. Nice! – Joël Apr 21 '20 at 10:13
8

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.

jcollado
  • 39,419
  • 8
  • 102
  • 133
  • 1
    This one looks nice, but lacks support for a major part of DBC: class invariants. I'll keep it in mind though. – ipartola Dec 19 '11 at 16:08
6

While not exactly design by contract, some testing frameworks favour property testing approach are very close conceptually.

Randomized testing for if certain properties hold in runtime allows to easily check:

  • invariants
  • domains of input and output values
  • other pre- and postconditions

For Python there are some QuickCheck-style testing frameworks:

sastanin
  • 40,473
  • 13
  • 103
  • 130