I spent a lot of May 2021 acting as a Section Leader in Stanford University's "CodeInPlace" project, which aimed to teach Python to a very large number of students world-wide (about 12,000), remotely, and staffed largely by volunteers. It was a great experience and I am posting here some of the general advice I gave to my students.
First, let’s take a step back and think about the nature of programming. The fundamental problem of programming is dealing with complexity: even comparatively modest programs can be more complicated than you are able to understand all at once. In our training course we might, at first, write programs that we can comprehend as a whole. In general, however, if you double the size of a program you more than double the amount information that you have to hold in your mind, because you also need to understand the connections between different parts of your program.
Imagine you are doing some gardening, and you carelessly leave the rake lying on the lawn, points up. Later that evening, in the dark, you go out to find the cat and step on the rake…. That is a long range connection between things you did this morning and consequences that come to pass as a result of your actions in the evening. Long-range connections in programs are the sort of places errors like to lurk - because if there are too many and they are too obscure we can’t remember them all and take them into account when we add new code. Hence, the craft of programming (and the term craft is used advisedly) is largely about controlling connections so you allow only those that are strictly necessary and you make them as transparent, understandable and memorable as possible.
The are several mental disciplines that help us to do this in programming: decomposition, for example, is a “divide and conquer” approach. We saw this in our Karel hospital build, when we could write a “helper” function that could be called to build the hospital at the current location of the robot. The other part of the decomposition was searching for beepers on the bottom row to location where we needed to put hospitals.
Pre- and Post-conditions are another discipline that helps us to be sure that the “helper” functions are going to do exactly the right thing when they are invoked. I like to think of them as contracts which define the interaction between the helper function and the party invoking the function. When you call the helper, you just want to know that it will achieve a certain outcome, and you don’t necessarily need to know exactly how it achieves that outcome. It is all the same to you. (For example, our hospitals are 3 beepers high by 2 wide. We could place these up column 1 then down column 2, or by doing row 1, then 2 and 3. The final effect is the same - as long as we get the robot back to the bottom of column 2.) The description of the final effect of the function (but not how it was achieved) is the post-condition.
From the view point of the helper-function, however, its interest in the contract is ensuring that it has the right conditions to start work. What does the client have to give me at the start in order for me to successfully carry out the work? That is the pre-condition of the contract. What must I give back to the client at the end? That is the post-condition.
There is rarely only one way of dividing up your program using pre- and post-conditions to connect different parts. Setting up good contracts is a matter of design choice: What are you going to do? What am I going to do? As a matter of experience, however, if the contracts seem to be difficult to write (that is, overly complex themselves) you have probably made the wrong design choices.
Learning to make the right design choices without large amounts of trial and error takes experience, and this is what makes expert programmers. It does not come quickly. Most of this experience applies completely independently of the programming language that you are employing. You can master the syntax of a new computing language in 40 or 50 hours of practice but mastering the art of expressing algorithms clearly and economically is a lot harder.
One of the problems, however, that you will experience is that stating pre- and post-conditions with precision, using the English language, in itself adds a layer of difficulty. English is often ambiguous, and it is easy to leave stuff out. That is why legal contracts seem to be written is such convoluted language. Engineers who have to get it right (e.g. those working on nuclear reactor protection systems) learn to describe their software using mathematical statements, and it is quite salutary to find, when start from informal English, how much you have actually left out.
It is, however, important to remember that something is better than nothing: I usually find that the effort of trying to construct even an informal pre-condition statement makes me think about what I am missing. As with legal contracts, you have to try to cover all contingencies or you will at some point be caught out by the loophole.
All this is especially important if you are working with other people: they need to know how to deliver on the implied commitments. Even if, however, you never envisage doing that, you should remember the likelihood that a year or two down the road you yourself might want to modify one of your programs. You may well conclude that you are dealing with the work of a stranger! (Much of my own professional software turned out to be surprisingly long-lived: there are programs I constructed over 30 years ago still being used every day.)