[Python-ideas] Why is design-by-contracts not widely adopted?

Steven D'Aprano steve at pearwood.info
Sat Sep 29 07:19:25 EDT 2018


On Wed, Sep 26, 2018 at 04:03:16PM +0100, Rhodri James wrote:

> Let's assume that the contracts are meaningful and useful (which I'm 
> pretty sure won't be 100% true; some people are bound to assume that 
> writing contracts means they don't have to think).

Contracts are a tool. We shouldn't refuse effective tools because some 
developers are too DailyWTF-worthy to use them. Why should the rest of 
us miss out because of their incompetence? Contracts are not rocket- 
science: there's nothing in them that most of us aren't already doing in 
an ad-hoc, clumsy manner by embedding the contracts in the docstring, 
inside the body of our methods, in external tests etc.


> Assuming that you 
> aren't doing some kind of wide-ranging static analysis (which doesn't 
> seem to be what we're talking about), all that the contracts have bought 
> you is the assurance that *this* invocation of the function with *these* 
> parameters giving *this* result is what you expected.  It does not say 
> anything about the reliability of the function in general.

This is virtually the complete opposite of what contracts give us. What 
you are describing is the problem with *unit testing*, not contracts.

Unit tests only tell us that our function works with the specific input 
the test uses. In contrast, contracts test the function with *every* 
input the function is invoked with.

(Up to the point that you disable checking, of course. Which is under 
your control: you decide when you are satisfied that the software is 
sufficiently bug-free to risk turning off checking.)

Both are a form of testing, of course. As they say, tests can only 
reveal the presence of bugs, they can't prove the absence of bugs. But 
correctness checkers are out of scope for this discussion.


> It seems to me that a lot of the DbC philosophy seems to assume that 
> functions are complex black-boxes whose behaviours are difficult to 
> grasp.

I can't imagine how you draw that conclusion. That's like saying that 
unit tests and documentation requires the assumption that functions are 
complex and difficult to grasp.

This introduction to DbC shows that contracts work with simple methods:

https://www.eiffel.com/values/design-by-contract/introduction/

and here's an example:


put (x: ELEMENT; key: STRING) is
		-- Insert x so that it will be retrievable through key.
	require
		count <= capacity
		not key.empty
	do
		... Some insertion algorithm ...
	ensure
		has (x)
		item (key) = x 
		count = old count + 1
	end

Two pre-conditions, and three post-conditions. That's hardly complex.

[Aside: and there's actually a bug in this. What if the key already 
exists? But this is from a tutorial, not production code. Cut them some 
slack.]

If I were writing this in Python, I'd write something like this:

def put(self, x, key):
    """Insert x so that it will be retrievable through key."""
    # Input checks are pre-conditions!
    if self.count > capacity:
        raise DatabaseFullError
    if not key:
        raise ValueError
    # .. Some insertion algorithm ...


and then stick the post-conditions in a unit test, usually in a 
completely different file:

class InsertTests(TestCase):
    def test_return_result(self):
        db = SomeDatabase()
        db.put("value", "key")
        self.AssertTrue("value" in db.values())
        self.AssertEqual(db["key"], "value")
        self.AssertEqual(db.count, 1)


Notice that my unit test is not actually checking at least one of the 
post-conditions, but a weaker, more specific version of it. The 
post-condition is that the count goes up by one on each insertion. My 
test only checks that the count is 1 after inserting into an empty 
database.


So what's wrong with the status quo?

- The pre-condition checks are embedded right there in the 
  method implementation, mixing up the core algorithm with the 
  associated error checking.

- Which in turn makes it hard to distinguish the checks from
  the implementation, and impossible to do so automatically.

- Half of the checks are very far away, in a separate file, assuming
  I even remembered or bothered to write the test.

- The post-conditions aren't checked unless I run my test suite, and
  then they only check the canned input in the test suite.

- The pre-conditions can't be easily disabled in production.

- No class invariants.

- Inheritance is not handled correctly.


The status quo is all so very ad-hoc and messy. Design By Contract 
syntax would allow (not force, allow!) us to add some structure to the 
code:

- requirements of the function
- the implementation of the function
- the promise made by the function

Most of us already think about these as three separate things, and 
document them as such. Our code should reflect the structure of how we 
think about the code.


> In my experience this is very rarely true.  Most functions I 
> write are fairly short and easily grokked, even if they do complicated 
> things.  That's part of the skill of breaking a problem down, IMHO; if 
> the function is long and horrible-looking, I've already got it wrong and 
> no amount of protective scaffolding like DbC is going to help.

That's like saying that if a function is horrible-looking, then there's 
no point in writing tests for it.

I'm not saying that contracts are only for horrible functions, but 
horrible functions are the ones which probably benefit the most from 
specifying exactly what they promise to do, and checking on every 
invocation that they live up to that promise.

 
> >It's the reason why type checking exists,
> 
> Except Python doesn't type check so much as try operations and see if 
> they work.

Python (the interpreter) does type checking. Any time you get a 
TypeError, that's a failed type check. And with type annotations, we can 
run a static type checker on our code too, which will catch many of 
these failures before we run the code.

Python code sometimes does type checking too, usually with isinstance. 
That's following the principle of Fail Fast, rather than waiting for 
some arbitrary exception deep inside the body of your function, you 
should fail early on bad input.


-- 
Steve


More information about the Python-ideas mailing list