Using Design by Contract in Python
Asked Answered
M

5

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?

Monophony answered 19/12, 2011 at 15:23 Comment(6)
Note that you can just inherit from TestCase, and include unit tests in any class.Gilud
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.Monophony
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.Gilud
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.Monophony
Everyone should probably be doing elements of both.Gilud
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.Monophony
H
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.

Haws answered 22/1, 2012 at 11:8 Comment(5)
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?".Monophony
@Monophony 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.Haws
Also the Covenant library (bitbucket.org/kisielk/covenant) from the link that @jcollado mentioned has invariants.Haws
If the sample code complains about "NameError: name 'precondition' is not defined", what am I missing? Thanks!Achieve
This was a while ago @Achieve but I think it relied on importing some code from the Python Decorator Library 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!Haws
S
15

In my experience, design-by-contract is worth doing, even without language support. For methods that aren't designed to be overridden, assertions and docstrings are sufficient for both pre- and post-conditions. For methods that are designed to be overridden, we split the method in two: a public method which checks the pre- and post-conditions, and a protected method which provides 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. They also mentioned the need for language support, claiming that assertions don't help enforce 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 would welcome that.

Stereography answered 9/8, 2013 at 20:49 Comment(4)
Great anecdote that actually answers the question.Infanta
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.Levenson
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.Levenson
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.Spindle
K
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).

Klingel answered 13/8, 2018 at 10:4 Comment(1)
Two years later, this seems the most interesting package, both as actively maintained, and providing most features. Nice!Idou
C
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.

Clarenceclarenceux answered 19/12, 2011 at 15:41 Comment(1)
This one looks nice, but lacks support for a major part of DBC: class invariants. I'll keep it in mind though.Monophony
G
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:

Gerber answered 24/1, 2012 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.