Good software is built from a good design. When we say clean code, it may be indicated that we are talking about good practices that relate only to the implementation details of the software, instead of its design. However, this assumption would be wrong since the code is not something different from the design—the code is the design. The code is probably the most detailed representation of the design.
A good quality software must be built around principles that serve as design tools and define the structure of how the code needs to be organized.
This article on design by contract is an excerpt from the book Clean Code in Python, Second Edition by Mariano Anaya – A guide that enables you to enhance your coding skills and implement the refactoring techniques and SOLID principles in Python 3.9
Design by contract
Some parts of the software we are working on are not meant to be called directly by users, but instead by other parts of the code. Such is the case when we divide the responsibilities of the application into different components or layers, and we have to think about the interaction between them.
We have to encapsulate some functionality behind each component and expose an interface to clients who are going to use that functionality, namely, an Application Programming Interface (API). The functions, classes, or methods we write for that component have a particular way of working under certain considerations that, if they are not met, will make our code crash. Conversely, clients calling that code expect a particular response, and any failure of our function to provide this would represent a defect.
That is to say that if, for example, we have a function that is expected to work with a series of parameters of type integers, and some other function invokes ours by passing strings, it is clear that it should not work as expected, but in reality, the function should not run at all because it was called incorrectly (the client made a mistake). This error should not pass silently.
Of course, when designing an API, the expected input, output, and side effects should be documented. But documentation cannot enforce the behavior of the software at runtime. These rules, what every part of the code expects in order to work properly and what the caller is expecting from them, should be part of the design, and here is where the concept of a contract comes into place.
The idea behind the DbC approach is that, instead of implicitly placing in the code what every party is expecting, both parties agree on a contract that, if violated, will raise an exception, clearly stating why it cannot continue.
In our context of design by contract, a contract is a construction that enforces some rules that must be honored during the communication of software components. A contract entails mainly preconditions and postconditions, but in some cases, invariants, and side effects are also described:
– Preconditions: We can say that these are all the checks the code will perform before running. It will check for all the conditions that have to be made before the function can proceed. In general, it’s implemented by validating the dataset provided in the parameters passed, but nothing should stop us from running all sorts of validations (for example, validating a set in a database, a file, or another method that was called before) if we consider that their side effects are overshadowed by the importance of such validations. Note that this imposes a constraint on the caller.
– Postconditions: The opposite of preconditions, here, the validations are done after the function call is returned. Postcondition validations are run to validate what the caller is expecting from this component.
– Invariants: Optionally, it would be a good idea to document, in the docstring of a function, the invariants, the things that are kept constant while the code of the function is running, as an expression of the logic of the function to be correct.
– Side effects: Optionally, we can mention any side effects of our code in the docstring.
While conceptually, all of these items form part of the contract for a software component, and this is what should go to the documentation of such a piece, only the first two (preconditions and postconditions) are to be enforced at a low level (code).
The reason why we would design by contract is that if errors occur, they must be easy to spot (and by noticing whether it was either the precondition or postcondition that failed, we will find the culprit much more easily) so that they can be quickly corrected. More importantly, we want critical parts of the code to avoid being executed under the wrong assumptions. This should help to clearly mark the limits for the responsibilities and errors if they occur, as opposed to something saying that this part of the application is failing. But the caller code provided the wrong arguments, so where should we apply the fix?
The idea is that preconditions bind the client (they have an obligation to meet them if they want to run some part of the code), whereas postconditions bind the component in relation to some guarantees that the client can verify and enforce.
This way, we can quickly identify responsibilities. If the precondition fails, we know it is due to a defect on the client. On the other hand, if the postcondition check fails, we know the problem is in the routine or class (supplier) itself. Specifically, regarding preconditions, it is important to highlight that they can be checked at runtime, and if they occur, the code that is being called should not be run at all (it does not make sense to run it because its conditions do not hold, and doing so might end up making things worse).
Preconditions are all of the guarantees a function or method expects to receive in order to work correctly. In general programming terms, this usually means providing data that is properly formed, for example, objects that are initialized, non-null values, and many more. For Python, in particular, being dynamically typed, this also means that sometimes we need to check for the exact type of data that is provided. This is not exactly the same as type checking, the mypy kind would do this, but rather verify the exact values that are needed.
Part of these checks can be detected early on by using static analysis tools, such as mypy, but these checks are not enough. A function should have proper validation for the information that it is going to handle.
Now, this poses the question of where to place the validation logic, depending on whether we let the clients validate all the data before calling the function, or allow this one to validate everything that it received prior to running its own logic. The former equates to a tolerant approach (because the function itself is still allowing any data, potentially malformed data as well), whereas the latter equates to a demanding approach.
For the purposes of this analysis, we prefer a demanding approach when it comes to DbC because it is usually the safest choice in terms of robustness, and usually the most common practice in the industry.
Regardless of the approach we decide to take, we should always keep in mind the non-redundancy principle, which states that the enforcement of each precondition for a function should be done by only one of the two parts of the contract, but not both. This means that we put the validation logic on the client, or we leave it to the function itself, but in no case should we duplicate it (which also relates to the DRY principle, which we will discuss later on in this chapter).
Postconditions are the part of the contract that is responsible for enforcing the state after the method or function has returned.
Assuming that the function or method has been called with the correct properties (that is, with its preconditions met), then the postconditions will guarantee that certain properties are preserved.
The idea is to use postconditions to check and validate everything that a client might need. If the method executed properly, and the postcondition validations pass, then any client calling that code should be able to work with the returned object without problems, as the contract has been fulfilled.
At the time of writing this book, a PEP-316, named Programming by Contract for Python, is deferred. That doesn’t mean that we cannot implement it in Python because, as introduced at the beginning of the chapter, this is a general design principle.
Probably the best way to enforce the design by contract principle is by adding control mechanisms to our methods, functions, and classes, and if they fail, raise a RuntimeError exception or ValueError. It’s hard to devise a general rule for the correct type of exception, as that would pretty much depend on the application in particular. These previously mentioned exceptions are the most common types of exception, but if they don’t fit accurately with the problem, creating a custom exception would be the best choice.
We would also like to keep the code as isolated as possible. That is, the code for the preconditions in one part, the one for the postconditions in another, and the core of the function separated. We could achieve this separation by creating smaller functions, but in some cases implementing a decorator would be an interesting alternative.
Design by contract – conclusions
The main value of this design principle is to effectively identify where the problem is. By defining a contract, when something fails at runtime, it will be clear what part of the code is broken, and what broke the contract.
As a result of following this principle, the code will be more robust. Each component is enforcing its own constraints and maintaining some invariants, and the program can be proven correct as long as these invariants are preserved.
About the Author
Mariano Anaya is a software engineer who spends most of his time creating software with Python and mentoring fellow programmers. Mariano’s main areas of interests besides Python are software architecture, functional programming, distributed systems, and speaking at conferences. He was a speaker at Euro Python 2016 and 2017. To know more about him, you can refer to his GitHub account with the username rmariano. His speakerdeck username is rmariano.
Looking to go beyond Design by Contract and learn more about Python? Here are a few related talks coming to ODSC East 2021 this March 30th – April 1st: