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

Hi Marko, I think there are several ways to approach this problem, though am not weighing in on whether DbC is a good thing in Python. I wrote a simple implementation of DbC which is currently a run-time checker. You could, with the appropriate tooling, validate statically too (as with all approaches). In my approach, I use a “proxy” object to allow the contract code to be defined at function definition time. It does mean that some things are not as pretty as one would like - anything that cannot be hooked into with magic methods i.e isinstance, but I think this is acceptable as it makes features like old easier. Also, one hopes that it encourages simpler contract checks as a side-effect. Feel free to take a look - https://github.com/agoose77/pyffel It is by no means well written, but a fun PoC nonetheless. Regards, Angus

Hi Barry, I think the main issue with pyffel is that it can not support function calls in general. If I understood it right, and Angus please correct me, you would need to wrap every function that you would call from within the contract. But the syntax is much nicer than icontract or dpcontracts (see these packages on pypi). What if we renamed "args" argument and "old" argument in those libraries to just "a" and "o", respectively? Maybe that gives readable code without too much noise: @requires(lambda self, a, o: self.sum == o.sum - a.amount) def withdraw(amount: int) -> None: ... There is this lambda keyword in front, but it's not too bad? I'll try to contact dpcontracts maintainers. Maybe it's possible to at least merge a couple of libraries into one and make it a de facto standard. @Agnus, would you also like to join the effort? Cheers, Marko Le lun. 24 sept. 2018 à 19:57, Barry Scott <barry@barrys-emacs.org> a écrit :

The args and old and not noise its easier to read the a and o. a and o as aliases for more descriptive names maybe, but not as the only name.
The lambda smells of internals that I should not have to care about being exposed. So -1 on lambda being required. Also being able to supply a list of conditions was a +1.

Barry Scott writes:
If you want to get rid of the lambda you can use strings and then 'eval' them in the condition. Adds overhead. If you want to avoid the extra runtime overhead of parsing expressions, it might be nice to prototype with MacroPy. This should also allow eliminating the lambda by folding it into the macro (I haven't used MacroPy but it got really good reviews by fans of that kind of thing). It would be possible to avoid decorator syntax if you want to with this implementation. I'm not sure that DbC is enough of a fit for Python that it's worth changing syntax to enable nice syntax natively, but detailed reports on a whole library (as long as it's not tiny) using DbC with a nice syntax (MacroPy would be cleaner, but I think it would be easy to "see through" the quoted conditions in an eval-based implementation) would go a long way to making me sit up and take notice. (I'm not influential enough to care about, but I suspect some committers would be impressed too. YMMV) Steve

Hi Steve, Thanks a lot for pointing us to macropy -- I was not aware of the library, it looks very interesting! Do you have any experience how macropy fit with current IDEs and static linters (pylint, mypy)? I fired up pylint and mypy on the sample code from their web site, played a bit with it and it seems that they go along well. I'm also a bit worried how macropy would work out in the libraries published to pypi -- imagine if many people start using contracts. Suddenly, all these libraries would not only depend on a contract library but on a macro library as well. Is that something we should care about? Potential dependency hell? (I already have a bad feeling about making icontract depend on asttokens and considerin-lining asttokens into icontract particularly for that reason). I'm also worried about this one (from https://macropy3.readthedocs.io/en/latest/overview.html):
Note that this means *you cannot use macros in a file that is run directly*, as it will not be passed through the import hooks.
That would make contracts unusable in any stand-alone script, right? Cheers, Marko On Tue, 25 Sep 2018 at 06:56, Stephen J. Turnbull < turnbull.stephen.fw@u.tsukuba.ac.jp> wrote:

Hi Steve and others, After some thinking, I'm coming to a conclusion that it might be wrong to focus too much about how the contracts are written -- as long as they are formal, easily transformable to another representation and fairly maintainable. Whether it's with a lambda, without, with "args" or "a", with "old" or "o" -- it does not matter that much as long as it is pragmatic and not something crazy complex. This would also mean that we should not add complexity (*e.g., *by adding macros) and limit the magic as much as possible. It is actually much more important in which form they are presented to the end-user. I already made an example with sphinx-icontract in a message before -- an improved version might use mathematical symbols (*e.g., *replace all() with ∀, replace len() with |.|, nicely use subscripts for ranges, use case distinction with curly bracket "{" instead of if.. else ..., etc.). This would make them even shorter and easier to parse. Let me iterate the example I already pasted in the thread before to highlight what I have in mind: packagery.resolve_initial_paths(*initial_paths*) Resolve the initial paths of the dependency graph by recursively adding *.py files beneath given directories. Parameters: *initial_paths* (List[Path]) – initial paths as absolute paths Return type: List[Path] Returns: list of initial files (*i.e.* no directories) Requires: - all(pth.is_absolute() for pth in initial_paths) Ensures: - all(pth in result for pth in initial_paths if pth.is_file()) (Initial files also in result) - len(result) >= len(initial_paths) if initial_paths else result == [] - all(pth.is_absolute() for pth in result) - all(pth.is_file() for pth in result) The contracts need to extend __doc__ of the function accordingly (and the contracts in __doc__ also need to reflect the inheritance of the contracts!), so that we can use help(). There should be also a plugin for Pycharm, Pydev, vim and emacs to show the contracts in an abbreviated and more readable form in the code and only show them in raw form when we want to edit them (*i.e., *when we move cursor over them). I suppose inheritance of contracts needs to be reflected in quick-inspection windows, but not in the code view. Diffs and github/bitbucket/... code reviews might be a bit cumbersome since they enforce the raw form of the contracts, but as long as syntax is pragmatic, I don't expect this to be a blocker. Is this a sane focus? Cheers, Marko On Tue, 25 Sep 2018 at 08:18, Marko Ristin-Kaufmann <marko.ristin@gmail.com> wrote:

Marko Ristin-Kaufmann writes:
Sorry, no. I was speaking as someone who is familiar with macros from Lisp but doesn't miss them in Python, and who also has been watching python-dev and python-ideas for about two decades now, so I've heard of things like MacroPy and know how the core developers think to a great extent.
That's right.
Is that something we should care about?
Yes. Most Pythonistas (at least at present) don't much like macros. They fear turning every program into its own domain-specific language. I can't claim much experience with dependency hell, but I think that's much less important from your point of view (see below). My point is mainly that, as you probably are becoming painfully aware, getting syntax changes into Python is a fairly drawnout process. For an example of the kind of presentation that motivates people to change their mind from the default state of "if it isn't in Python yet, YAGNI" to "yes, let's do *this* one", see https://www.python.org/dev/peps/pep-0572/#appendix-a-tim-peters-s-findings Warning: Tim Peters is legendary, though still active occasionally. All he has to do is post to get people to take notice. But this Appendix is an example of why he gets that kind of R-E-S-P-E-C-T.[1] So the whole thing is a secret plot ;-) to present the most beautiful syntax possible in your PEP (which will *not* be about DbC, but rather about a small set of enabling syntax changes, hopefully a singleton), along with an extended example, or a representative sample, of usage. Because you have a working implementation using MacroPy (or the less pretty[2] but fewer dependencies version based on condition strings and eval) people can actually try it on their own code and (you hope, they don't :-) they find a nestful of bugs by using it.
I don't think so. First, inlining an existing library is almost always a bad idea. As for the main point, if the user sticks to one major revision, and only upgrades to compatible bugfixes in the Python+stdlib distribution, I don't see why two or three libraries would be a major issue for a feature that the developer/project uses extremely frequently. I've rarely experienced dependency hell, and in both cases it was web frameworks (Django and Zope, to be specific, and the dependencies involved were more or less internal to those frameworks). If you or people you trust have other experience, forget what I just said. :-) Of course it depends on the library, but as long as the library is pretty strict about backward compatibility, you can upgrade it and get new functionality for other callers in your code base (which are likely to appear, you know -- human beings cannot stand to leave a tool unused once they install it!)
Yes, but really, no: # The run.py described by the MacroPy docs assumes a script that # runs by just importing it. I don't have time to work out # whether that makes more sense. This idiom of importing just a # couple of libraries, and then invoking a function with a # conventional name such as "run" or "process" is quite common. # If you have docutils install, check out rstpep2html.py. import macropy.activate from my_contractful_library import main main() and away you go. 5 years from now that script will be a badge of honor among Pythonic DbCers, and you won't be willing to give it up! Just kidding, of course -- the ideal outcome is that the use case is sufficiently persuasive to justify a syntax change so you don't need MacroPy, or, perhaps some genius will come along and provide some obscure construct that is already legal syntax! HTH Footnotes: [1] R.I.P. Aretha! [2] Urk, I just realized there's another weakness to strings: you get no help on checking their syntax from the compiler. For a proof-of- concept that's OK, but if you end up using the DbC library in your codebase for a couple years while the needed syntax change gathers support, that would be really bad.

Hi Steve, I'll give it a shot and implement a proof-of-concept icontrac-macro library based on macropy and see if that works. I'll keep you posted. Cheers, Marko On Tue, 25 Sep 2018 at 12:08, Stephen J. Turnbull < turnbull.stephen.fw@u.tsukuba.ac.jp> wrote:

Hi Mario, yes I'd pass in some kind of 'old' object as a proxy to the old object state. My demo can handle function calls, unless they themselves ultimately call something which can't be proxies e.g is instance (which delegates to the test class, not the instance), or boolean evaluation of some expression (e.g an if block). I don't think that this is awful - contracts should probably be fairly concise while expressive - but definitely non-ideal. I haven't really time to work on this at the moment; I admit, it was a specific problem of interest, rather than a domain I have much experience with. In fact, it was probably an excuse to overload all of the operators on an object! Kind regards, Angus On Mon, 24 Sep 2018, 20:09 Marko Ristin-Kaufmann, <marko.ristin@gmail.com> wrote:

Angus Hollands writes:
yes I'd pass in some kind of 'old' object as a proxy to the old object state.
Mostly you shouldn't need to do this, you can copy the state: def method(self, args): import copy old = copy.deepcopy(self) This is easy but verbose to do with a decorator, and I imagine a bunch of issues about the 'old' object with multiple decorators, so I omit it here. You might want a variety of such decorators. Ie, using copy.copy vs copy.deepcopy vs a special-case copy for a particular class because there are large objects that are actually constant that you don't want to copy (an "is" test would be enough, so the copy would actually implement part of the contract). Or the copy function could be an argument to the decorator or a method on the object.

Hi Barry, I think the main issue with pyffel is that it can not support function calls in general. If I understood it right, and Angus please correct me, you would need to wrap every function that you would call from within the contract. But the syntax is much nicer than icontract or dpcontracts (see these packages on pypi). What if we renamed "args" argument and "old" argument in those libraries to just "a" and "o", respectively? Maybe that gives readable code without too much noise: @requires(lambda self, a, o: self.sum == o.sum - a.amount) def withdraw(amount: int) -> None: ... There is this lambda keyword in front, but it's not too bad? I'll try to contact dpcontracts maintainers. Maybe it's possible to at least merge a couple of libraries into one and make it a de facto standard. @Agnus, would you also like to join the effort? Cheers, Marko Le lun. 24 sept. 2018 à 19:57, Barry Scott <barry@barrys-emacs.org> a écrit :

The args and old and not noise its easier to read the a and o. a and o as aliases for more descriptive names maybe, but not as the only name.
The lambda smells of internals that I should not have to care about being exposed. So -1 on lambda being required. Also being able to supply a list of conditions was a +1.

Barry Scott writes:
If you want to get rid of the lambda you can use strings and then 'eval' them in the condition. Adds overhead. If you want to avoid the extra runtime overhead of parsing expressions, it might be nice to prototype with MacroPy. This should also allow eliminating the lambda by folding it into the macro (I haven't used MacroPy but it got really good reviews by fans of that kind of thing). It would be possible to avoid decorator syntax if you want to with this implementation. I'm not sure that DbC is enough of a fit for Python that it's worth changing syntax to enable nice syntax natively, but detailed reports on a whole library (as long as it's not tiny) using DbC with a nice syntax (MacroPy would be cleaner, but I think it would be easy to "see through" the quoted conditions in an eval-based implementation) would go a long way to making me sit up and take notice. (I'm not influential enough to care about, but I suspect some committers would be impressed too. YMMV) Steve

Hi Steve, Thanks a lot for pointing us to macropy -- I was not aware of the library, it looks very interesting! Do you have any experience how macropy fit with current IDEs and static linters (pylint, mypy)? I fired up pylint and mypy on the sample code from their web site, played a bit with it and it seems that they go along well. I'm also a bit worried how macropy would work out in the libraries published to pypi -- imagine if many people start using contracts. Suddenly, all these libraries would not only depend on a contract library but on a macro library as well. Is that something we should care about? Potential dependency hell? (I already have a bad feeling about making icontract depend on asttokens and considerin-lining asttokens into icontract particularly for that reason). I'm also worried about this one (from https://macropy3.readthedocs.io/en/latest/overview.html):
Note that this means *you cannot use macros in a file that is run directly*, as it will not be passed through the import hooks.
That would make contracts unusable in any stand-alone script, right? Cheers, Marko On Tue, 25 Sep 2018 at 06:56, Stephen J. Turnbull < turnbull.stephen.fw@u.tsukuba.ac.jp> wrote:

Hi Steve and others, After some thinking, I'm coming to a conclusion that it might be wrong to focus too much about how the contracts are written -- as long as they are formal, easily transformable to another representation and fairly maintainable. Whether it's with a lambda, without, with "args" or "a", with "old" or "o" -- it does not matter that much as long as it is pragmatic and not something crazy complex. This would also mean that we should not add complexity (*e.g., *by adding macros) and limit the magic as much as possible. It is actually much more important in which form they are presented to the end-user. I already made an example with sphinx-icontract in a message before -- an improved version might use mathematical symbols (*e.g., *replace all() with ∀, replace len() with |.|, nicely use subscripts for ranges, use case distinction with curly bracket "{" instead of if.. else ..., etc.). This would make them even shorter and easier to parse. Let me iterate the example I already pasted in the thread before to highlight what I have in mind: packagery.resolve_initial_paths(*initial_paths*) Resolve the initial paths of the dependency graph by recursively adding *.py files beneath given directories. Parameters: *initial_paths* (List[Path]) – initial paths as absolute paths Return type: List[Path] Returns: list of initial files (*i.e.* no directories) Requires: - all(pth.is_absolute() for pth in initial_paths) Ensures: - all(pth in result for pth in initial_paths if pth.is_file()) (Initial files also in result) - len(result) >= len(initial_paths) if initial_paths else result == [] - all(pth.is_absolute() for pth in result) - all(pth.is_file() for pth in result) The contracts need to extend __doc__ of the function accordingly (and the contracts in __doc__ also need to reflect the inheritance of the contracts!), so that we can use help(). There should be also a plugin for Pycharm, Pydev, vim and emacs to show the contracts in an abbreviated and more readable form in the code and only show them in raw form when we want to edit them (*i.e., *when we move cursor over them). I suppose inheritance of contracts needs to be reflected in quick-inspection windows, but not in the code view. Diffs and github/bitbucket/... code reviews might be a bit cumbersome since they enforce the raw form of the contracts, but as long as syntax is pragmatic, I don't expect this to be a blocker. Is this a sane focus? Cheers, Marko On Tue, 25 Sep 2018 at 08:18, Marko Ristin-Kaufmann <marko.ristin@gmail.com> wrote:

Marko Ristin-Kaufmann writes:
Sorry, no. I was speaking as someone who is familiar with macros from Lisp but doesn't miss them in Python, and who also has been watching python-dev and python-ideas for about two decades now, so I've heard of things like MacroPy and know how the core developers think to a great extent.
That's right.
Is that something we should care about?
Yes. Most Pythonistas (at least at present) don't much like macros. They fear turning every program into its own domain-specific language. I can't claim much experience with dependency hell, but I think that's much less important from your point of view (see below). My point is mainly that, as you probably are becoming painfully aware, getting syntax changes into Python is a fairly drawnout process. For an example of the kind of presentation that motivates people to change their mind from the default state of "if it isn't in Python yet, YAGNI" to "yes, let's do *this* one", see https://www.python.org/dev/peps/pep-0572/#appendix-a-tim-peters-s-findings Warning: Tim Peters is legendary, though still active occasionally. All he has to do is post to get people to take notice. But this Appendix is an example of why he gets that kind of R-E-S-P-E-C-T.[1] So the whole thing is a secret plot ;-) to present the most beautiful syntax possible in your PEP (which will *not* be about DbC, but rather about a small set of enabling syntax changes, hopefully a singleton), along with an extended example, or a representative sample, of usage. Because you have a working implementation using MacroPy (or the less pretty[2] but fewer dependencies version based on condition strings and eval) people can actually try it on their own code and (you hope, they don't :-) they find a nestful of bugs by using it.
I don't think so. First, inlining an existing library is almost always a bad idea. As for the main point, if the user sticks to one major revision, and only upgrades to compatible bugfixes in the Python+stdlib distribution, I don't see why two or three libraries would be a major issue for a feature that the developer/project uses extremely frequently. I've rarely experienced dependency hell, and in both cases it was web frameworks (Django and Zope, to be specific, and the dependencies involved were more or less internal to those frameworks). If you or people you trust have other experience, forget what I just said. :-) Of course it depends on the library, but as long as the library is pretty strict about backward compatibility, you can upgrade it and get new functionality for other callers in your code base (which are likely to appear, you know -- human beings cannot stand to leave a tool unused once they install it!)
Yes, but really, no: # The run.py described by the MacroPy docs assumes a script that # runs by just importing it. I don't have time to work out # whether that makes more sense. This idiom of importing just a # couple of libraries, and then invoking a function with a # conventional name such as "run" or "process" is quite common. # If you have docutils install, check out rstpep2html.py. import macropy.activate from my_contractful_library import main main() and away you go. 5 years from now that script will be a badge of honor among Pythonic DbCers, and you won't be willing to give it up! Just kidding, of course -- the ideal outcome is that the use case is sufficiently persuasive to justify a syntax change so you don't need MacroPy, or, perhaps some genius will come along and provide some obscure construct that is already legal syntax! HTH Footnotes: [1] R.I.P. Aretha! [2] Urk, I just realized there's another weakness to strings: you get no help on checking their syntax from the compiler. For a proof-of- concept that's OK, but if you end up using the DbC library in your codebase for a couple years while the needed syntax change gathers support, that would be really bad.

Hi Steve, I'll give it a shot and implement a proof-of-concept icontrac-macro library based on macropy and see if that works. I'll keep you posted. Cheers, Marko On Tue, 25 Sep 2018 at 12:08, Stephen J. Turnbull < turnbull.stephen.fw@u.tsukuba.ac.jp> wrote:

Hi Mario, yes I'd pass in some kind of 'old' object as a proxy to the old object state. My demo can handle function calls, unless they themselves ultimately call something which can't be proxies e.g is instance (which delegates to the test class, not the instance), or boolean evaluation of some expression (e.g an if block). I don't think that this is awful - contracts should probably be fairly concise while expressive - but definitely non-ideal. I haven't really time to work on this at the moment; I admit, it was a specific problem of interest, rather than a domain I have much experience with. In fact, it was probably an excuse to overload all of the operators on an object! Kind regards, Angus On Mon, 24 Sep 2018, 20:09 Marko Ristin-Kaufmann, <marko.ristin@gmail.com> wrote:

Angus Hollands writes:
yes I'd pass in some kind of 'old' object as a proxy to the old object state.
Mostly you shouldn't need to do this, you can copy the state: def method(self, args): import copy old = copy.deepcopy(self) This is easy but verbose to do with a decorator, and I imagine a bunch of issues about the 'old' object with multiple decorators, so I omit it here. You might want a variety of such decorators. Ie, using copy.copy vs copy.deepcopy vs a special-case copy for a particular class because there are large objects that are actually constant that you don't want to copy (an "is" test would be enough, so the copy would actually implement part of the contract). Or the copy function could be an argument to the decorator or a method on the object.
participants (4)
-
Angus Hollands
-
Barry Scott
-
Marko Ristin-Kaufmann
-
Stephen J. Turnbull