[Python-ideas] Why is design-by-contracts not widely adopted?
steve at pearwood.info
Mon Oct 1 21:12:07 EDT 2018
On Mon, Oct 01, 2018 at 12:36:18PM -0400, David Mertz wrote:
> There's some saying/joke about software testing along the lines of:
> For an argument that should be in range 1-100:
> try 50; try 1; try 100
> try 101; try 0;
> try -1,
> try 3.14
> try 1+1j;
> try the string "fish",
> try a null pointer;
> Many of those oddball cases can easily be in a list of values to test in
> unit tests, but may be impossible or unlikely to make it to the function
> call in the normal/possible flow of the program.
Right. And for a *library*, you must handle out of range errors, because
the library cannot trust the input passed to it by third parties. If a
third party passes "fish" as an argument, that's not a bug in the
library and the library cannot fix it, it should handle the error
gracefully with a sensible error message.
But for an *application*, the internal routines are completely under
control of the one developer team. If an internal routine passes "fish",
that's a bug that the dev team can and ought to fix. Once that bug is
fixed, there's no need to check for "fish" in production code. Its a
waste of time.
(In principle at least -- it depends on how confident you or your QA
team are that all the bugs are ironed out. Do you ship your apps with
debugging turned off? Then you might ship with contract checking turned
off, or at least dialled back to a lower level.)
For an application, it doesn't matter if my function sets the computer
on fire when passed the string "fish", if there is no way for the
application to pass that string to the function. If it can't happen, it
can't happen and there's no need to defend against it beyond a
(Modulo comments about how confident you are about the software
For a library, anyone can pass "fish" to anything, so you better handle
it gracefully rather than setting the computer on fire.
For application developers, the point of contracts is to make error
conditions *impossible* rather than to just defend against them, so you
can turn off the defensive error checking in production because they
> > I'm curious. When you write a function or method, do you include input
> > checks? Here's an example from the Python stdlib (docstring removed for
> > brevity):
> > # bisect.py
> > def insort_right(a, x, lo=0, hi=None):
> > if lo < 0:
> > raise ValueError('lo must be non-negative')
> > if hi is None:
> > hi = len(a)
> > while lo < hi:
> > mid = (lo+hi)//2
> > if x < a[mid]: hi = mid
> > else: lo = mid+1
> > a.insert(lo, x)
> > Do you consider that check for lo < 0 to be disruptive? How would you
> > put that in a unit test?
> I definitely put in checks like that. However, I might well write a test
> assert lo >= 0, "lo must be non-negative"
> That would allow disabling the check for production.
For a library function, I would consider that an abuse of assert, since:
(1) It raises the wrong exception (AssertionError);
(2) It can be disabled;
(3) And it sends the wrong message, telling the reader that lo is an
internal implementation detail rather than part of the function's
> However, I presume
> the stdlib does this for a reason; it presumably wants to allow callers to
> catch a specific exception. I haven't seen anything in the contract
> discussion that allows raising a particular exception when a contract is
> violated, only a general one for all contracts.
I think that the Eiffel philosophy is that all contract violations are
assertion errors, but I'm not really confident in my understanding of
Eiffel's exception mechanism. It doesn't have a generalised try...except
statement like Python.
If Python had contracts, I'd want to see:
- by default, contract violations raise specific subclasses
of AssertionError, e.g. RequireError, EnsureError;
- but there ought to be a mechanism to allow specific errors
to raise more specific exceptions.
I haven't given any thought to that mechanism. It might simply be "the
status quo" -- there's no reason why we *must* move defensive input
checking into the (hypothetical) require block just because it is
available. You can still write your test the old fashioned way in the
body of the function and raise the exception of your choice.
> The case of 'if hi is None' is different. It's remediation of a missing
> value where it's perfectly fine to impute an unspecified value. So that
> would be a poor contract.
Since the check for None is not error checking at all, I didn't talk
about it or make it a precondition test.
More information about the Python-ideas