[Python-ideas] Contracts in python -- a report & next steps

Marko Ristin-Kaufmann marko.ristin at gmail.com
Wed Oct 24 14:23:12 EDT 2018


Hi Chris,

If not, all your proposed benefits can be achieved at the level of a
> single project, by just saying "we're going to use THIS contracts
> library", unless I'm misunderstanding something here.
>


I think we are having a big disconnect in the discussion. Please apologize
for my vagueness and point me where you would like me to elaborate more.
Let me try to have another iteration where I'll try to paraphrase myself
and illustrate what I think that you are thinking.

I imagine that you conceive contracts purely as an approach to a testing to
be applied to a single project. I'm not talking about that. I'm talking
about two packages on pypi, both specifying contracts, each developed by a
separate team from different organizations. Let's call these packages
package_a and package_b, respectively (and team A and B, analogously).

Imagine now that the contracts are not standardized. For example, team A
uses dpcontracts whereas team B uses icontract. Enter team C working on the
package_c.

There is a class package_c.SomeClass that needs to inherit both from
package_a.some_module.BaseClass and from
package_b.another_module.AnotherBaseClass. Imagine the mess that developers
in team C have to deal with -- how are contracts supposed to be
strengthened and weakened in package_c.SomeClass? A nightmare with
different contract libraries. Even if they don't have to work with multiple
inheritance, they would need to use different syntaxes for different
concrete classes inheriting from the two packages, respectively. And now
think of contract groups that can be specified and applied to functions:

@require(...)
@require(...)
@ensure(...)
def some_func(...):
    ...

@enforce_contracts_of(some_func)
@require(...)
def some_other_func(...):
    ...

some_group = [
    require(...),
    ensure(...),
    ensure(...)
]

@enforce_contracts(some_group)
@ensure(...)
def yet_another_function(...):
    ...


How are these contract groups supposed to play together between package_a
and package_b when applied in package_c? (A side remark: the name and
implementation of "enforce_contracts_of" and "enforce_contracts" are still
in discussion for icontract and have not been implemented yet.)

Furthermore, what about toggling contracts? Team C needs to know both how
to toggle the contracts of package_a.some_module and also how to toggle the
contracts of package_b.another_module. They need to be aware of and
probably set two different environment variables with different toggling
granularities *etc*. What if the toggling conflicts?

Even if they both used, say, icontract. Package_a might require icontract
1.x whereas package_b requires 2.x. This results in a dependency hell. A
standardized lib would force both packages to use the same version of
contract lib (or not be supported in that given python version at all) --
the compatibility would be dictated by the python version, not by the
version of a non-standard contract lib.

Let's focus now on the team working on mypy. For example, consider the code:

@invariant(lambda self: self.something is not None or self.another is not None)
class SomeClass:
    ...

# later in code somewhere
def some_func():
    s = SomeClass(...)
    if s.something is None:
        # mypy knows here that s.another is not None.
        ...

But if there existed multiple non-standard contract libraries, can we
really expect that mypy deals with all of them? I couldn't possibly imagine
that.

If these points are still not clear -- imagine having multiple
non-standard, but widely used libraries and approaches for abstract base
classes or object-oriented programming. A horrifying thought :) I'd
probably even prefer the python world without contracts than with a
widespread use of multiple contract libraries.

If pythonistas are to use contracts more widely without stomping on each
other's feet, there needs to be some form of standardization so that
different packages can inter-operate. There needs to be a convention how to
toggle the contracts at different levels of granularity (individual
contracts, module-level, pypi package-level, only in testing *etc.*).
Environment variables? Setting global variables before importing a module?
Some mechanism I'm not aware of? Are the contracts on or off by default?

There are so many other details that needs to be fleshed out and this needs
to be a collective effort if it is to succeed. I don't have the knowledge
to figure out all the conceptual details alone and need help from more
experienced people.

---

Now a completely different point. Whether we write contracts *for* the
modules of the standard library is another discussion. It is not directly
connected with the standardization of contracts (and also not part of the
current thread, IMO). To write contracts or not to write contracts for a
standard library is a per-module decision and concerns the authors and
users of each module in separation. Some modules might include contracts
that are run only in test mode (*e.g., *in case of icontract, that would be
the situation when the environment variable ICONTRACT_SLOW is set); other
modules might verify only some of them in production; yet other modules
might always verify all the contracts. Or standard lib might take the
approach that each module has a corresponding environment variable to turn *on
*the contracts, while its contracts are disabled by default.

In current implementation of icontract, there is a negligible overhead at
the moment during the import of a module with disabled contracts (numbers
from the previous message; average over 10 runs, in milliseconds):
Duration to import the module functions_100_with_no_contract : 795.59 ±
10.47

Duration to import the module functions_100_with_1_disabled_contract :
833.60 ± 32.07
Duration to import the module functions_100_with_5_disabled_contracts :
851.31 ± 66.93
Duration to import the module functions_100_with_10_disabled_contracts :
897.90 ± 143.02

The whole import overhead is attributed to the interpreter having to parse
the decorators and the call to the decorator at the import time (when the
decorator immediately returns the wrapped function). If this overhead is
unacceptable -- then we need to figure out how to circumvent the parsing of
contracts in the interpreter directly and ignore the contract decorators
which are disabled. Or have a code base which is shipped for production and
another one which is shipped for testing (the former would have the
contracts automatically stripped out). This overhead is a limitation of *any
*decorator -- *e.g.,* if abstract base classes are OK, I don't see why
contracts wouldn't be.

I could think of many other optimizations in CPython (*e.g., *it could
in-line the code to verify the condition in the function body to avoid
making two calls, one to the wrapper and another to the wrapped function),
but this is really not the topic of the current thread (please feel free to
fork to another thread if you'd like to discuss that particular topic).

---

My conclusion is at this point is that the best course of action would be
that other teams pick up a contract library (now that there is at least
icontract library with a full feature set for design-by-contract akin to
Eiffel and other languages), and present us how they use contracts and what
problems they were facing (*e.g., *were there any problems with the
toggling granularity? readability? maintainability?). Then we decide how to
proceed.

What are the opinions of the other folks that would like to have a
standardized contract library?

Cheers,
Marko
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20181024/2fb848c1/attachment-0001.html>


More information about the Python-ideas mailing list