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

Steven D'Aprano steve at pearwood.info
Tue Oct 9 03:23:16 EDT 2018


On Mon, Oct 08, 2018 at 03:34:51PM -0400, Terry Reedy wrote:

> >> I said above that functions may be specified by
> >> process rather than result.
> >
> > Fine. What of it? Can you describe what the function does?
> >
> > "It sorts the list in place."
> >
> > "It deletes the given record from the database."
> 
> > These are all post-conditions.
> 
> No they are not.

Yes they are; they are textual descriptions of the post-condition, not 
executable code, but since software contracts are both specification and 
code, they certainly count as post-conditions.

By the way, you seem to have deleted one of my examples, leaving only 
the two relatively simple ones. I don't understand why you deleted the 
example, but here it is again:

"It deducts the given amount from Account A and transfers it
to Account B, guaranteeing that either both transactions occur
or neither of them, but never one and not the other."



> They are descriptions of the process.  Additional 
> mental work is required to turn them into formal descriptions of the 
> result that can be coded.

Yes. That's called "programming". You might have done it from time to 
time :-)



> Marko appears to claim that such coded formal 
> descriptions are easier to read and understand than the short English 
> description.  I disagree.

What does "sort in place"? *Precisely*? How do you know if the software 
*actually does* sort in place?

They are not rhetorical questions.

You can document that the function sorts in place, but if it actually 
reverses a copy of the list, how would you know?

How do we know the software meets the specified requirements if you 
haven't nailed down the specification, and if the specification exists 
only in documentation and is never checked in the code?

Design By Contract is a methodology that helps ensure that the 
specification is (1) nailed down and (2) actually checked.


> It is therefore not obvious to me that the 
> extra work is worthwhile.

Tell me how you, personally, would find out whether or not the function
actually sorts the list in place. What process would YOU do?

As I see it, you either have to take it on trust, or you have to write 
tests. Would you argue that the "extra work" to write tests are not 
worthwhile?



> >>def append_first(seq):
> >>     "Append seq[0] to seq."
> >[...]
> 
> The snipped body (revised to omit irrelevant 'return')
>     seq.append(seq[0])
> 
> >>But with duck-typing, no post condition is possible.
> >
> >That's incorrect.
> >
> >def append_first(seq):
> >     require:
> >         len(seq) > 0
> 
> seq does not neccessarily have a __len__ method

Yes it must, because I wrote the specification and I demand that it 
must support len.

You lost your opportunity to write the spec yourself when you wrongly 
said no post-condition is possible. I just showed that post-conditions 
ARE possible, by writing some.

We could debate what those conditions are: can we just test for the 
Sequence interface, using isinstance() and the Sequence ABC? Do I need 
to bother checking that seq supports __getitem__?

But I'm not really interested in getting bogged down in the minutia of 
what specific post-conditions need to go with this toy function. Its a 
made-up example, so who cares? In practice, you specify in as much 
detail as you need. You can always come back and add more detail later.


> >         hasattr(seq, "append")
> 
> The actual precondition is that seq[0] be in the domain of seq.append. 

That is not part of the contract of the append_first function. It 
doesn't care about any domain limits of seq. If an object could be 
appended into seq[0] in the first place, surely it can append again 
later. We don't have to try to detect every imaginable bizarre weird 
piece of code intentionally written to perplex us.


> The only absolutely sure way to test this is to run the code body.

Sure. So what? The code body does actually get run you know. If it 
raises an exception because seq can't append seq[0] to itself, that's a 
bug that needs to be fixed.


> Or 
> one could run seq[0] and check it against the preconditions, if formally 
> specified, of seq.append.

That is not the responsibility of the append_first function.


> >     ensure:
> >         len(seq) == len(OLD.seq) + 1
> >         seq[0] == seq[-1]
> 
> Not even all sequences implement negative indexing.

I don't care. Since I wrote the spec, I demand that only those which 
meet my requirements are used.

But okay, fine, I'll re-write it:

    seq[0] == seq[len(seq)-1]

and then you can tell me off for writing un-Pythonic code, and then I'll 
change it back.


> This is true for lists, as I said, but not for every object the meets 
> the preconditions.  As others have said, duck typing means that we don't 
> know what unexpected things methods of user-defined classes might do.

That's not how we use duck-typing. When I call len(obj), I don't expect 
that it will format my hard drive, even though obj.__len__ might do 
that, and if it did, I would treat it as a bug. Why am I passing such an 
object as input? That makes no sense. I *don't want* that behaviour, so 
why would I pass an object that does that?

I wouldn't say "Oh well, that means that formatting my head drive must 
be the correct behaviour for my application, good thing I have backups."

I would say "That's a bug. Fix it."

Here is the scenario: I am writing an application that has some specific 
purpose. I don't know, let's say it is a payroll application. It talks 
to a database and generates payroll reports or something. Whatever. The 
point is it has some *concrete* purpose, it's not just Mickey Mouse toy 
functions.

As part of that application, I need (for some unspecified reason!) to 
take a sequence, extract the first item, and append it to the end of the 
sequence. Hence you append_first function. Why? Who knows. Let's say it 
makes sense in context. I trust you.

In context, the sequence I pass to that function is not going to be 
"some arbitrary object that could do absolutely anything". It is going 
to be from a tiny subset of *useful* sequences, specifically the ones 
which actually do append the first item to the end of sequence. Why 
would I write, and then use, a class that does something unhelpful? Am I 
a malicious coder trying to get myself fired?

The purpose of the contract is to detect ACCIDENTAL bugs early, not to 
defend against intentional self-sabotage.


-- 
Steve


More information about the Python-ideas mailing list