What do to when things go wrong#

Some general advice#

First, we bring to mind the words on the cover of The Hitchhiker’s Guide to the Galaxy: “Don’t panic”.

That’s not always a useful thing to say to someone who actually is panicking, of course, but we hope that you are not really panicking, and what you need to hear is that you are already doing the right thing.

We (the authors) are all mathematical programmers, some with a lot of experience in different computer languages, and “to a first approximation” we always get the code wrong on the first attempt. We always have to find “bugs” and fix them. In fact, we do not know of any programmers at all who can count on writing bug-free code on the first try. Not even Donald Knuth, regarded by some as being one of the best programmers on the planet.

So, the first thing to do when you have finished (heh, “finished”) writing your code is to assume that it is not working. Then you try it. When it doesn’t work (probably with a syntax error), you are not surprised (or disappointed). You will, of course, be annoyed.

Next, you swat the bugs you see in your code (the most frustrating ones of all are, of course, not where you are looking) and try it again. A few iterations of this and you will finally (heh, “finally”) arrive at code that might be working. At least it runs without throwing an error message at you.

You will need to test the code, and test it quite thoroughly. This is true even if you have proved mathematically that your code is correct. Look at Fibonacci Activity Report 10 for a clear example of this.

Then, when your code runs without throwing errors at you, and passes your tests, you can begin to use it. You might still have bugs (of course) but every successful run will increase your confidence.

“Writing [code] is nature’s way of showing us how sloppy our thinking is.”

—attributed to Leslie Lamport

There’s some truth in that quotation from Leslie Lamport, but there’s some victim-blaming in there, too. Programming is hard, but it’s even harder to design programming languages that are truly “intuitive”. (We don’t know of any, in fact.) The truth is that the designer(s) of the language in question, be it Python or Maple or whatever, came to their decisions based on a way of thinking about things that you (probably) won’t share, at least at first. To take a simple example, non-programmers think of the “=” sign, invented by Robert Recorde in 1557 “bicause noe 2 thynges can be moare equalle”, as defining equality or identity. But in Python it means assignment: replace the contents of the thing on the left by the thing indicated on the right. It’s an action, not a statement.

While we are on the topic, if you want to check equality, you have to use ==. If you try to use = you will get an error message, like so:

x = 3
if x=3 :
    print("x is three")
else:
    print("x is not three")
  Input In [1]
    if x=3 :
       ^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?

That was an expected error message, but it not only mentions == but also the “walrus operator” :=. More about the walrus operator.

You will have to get used to guessing how the designers thought and that means looking at a lot of code and reading a lot of error messages. Following and modifying good examples is a good way to learn good habits of thought. Sometimes, though, you will look at some code construct and think “Whoever thought that was a good idea??” People can and do think differently about programming. This is why we have so many different languages, from C to Haskell, from assembly language to Rust. RMC’s favourite was FORTH, a language invented for control of telescopes. Niche, but elegant. And Leo Brodie’s two books on it freely available at that link can really teach you something about writing good code in any language.

One of the things that distinguishes Python is that indentation has semantic meaning. This is quite unlike most other languages. In Maple, for instance (which on a deep structure level is actually quite similar to Python) one ends loops with an appropriate end statement (and similarly for if statements and for procedures). But for readability people use indentation to indicate to the human reader—usually your own future self—what is going on. For Python they just decided to make it mandatory. Another is “indexing from zero” by default. One gets used to it.

The science of Human-Computer Interaction includes the study of programming language design, by the way. It’s very hard.

After you have had some practice programming, here’s a good read#

Best Practices for Scientific Computing

One of the pieces of advice that we like the best is “Write programs for people, not computers”. This is their first bullet point in their Box 1 summary and it deserves that place.

Another piece of advice from that paper (point 6 in their Box 1) is to write your program in the highest-level programming language available for the purpose. We haven’t spoken much here about “high-level” versus “low-level” languages, but Python (and Maple) are very high-level languages. Each statement has a lot of power, and before its execution on a physical machine it gets translated into the ultra low-level language that the computer actually understands. The advantages are that it is easier to match how a human thinks about the problem to code that actually works. The disadvantages include that (typically) computing time is not optimized; a low-level code to do the same thing typically runs much faster, sometimes orders of magnitude faster. But the key piece of advice (also in point 6) is to get it working first, and optimize later.

Writing low-level code directly is possible, and some humans are actually quite good at it, but doing so typically takes a much longer time than a human writing high-level code and subsequently optimizing it (or just optimizing the bits that actually need optimizing). So while the computing time (ie the time the program takes to run on the final run) might be lower the first way, the overall time taken from the start of the project to the final report can be quite a bit shorter by using a high level language.

And the result is more reproducible, because high-level programming is typically much more intelligible to other humans (including your future self).

Some specific advice#

The simplest thing to do with a syntax error is to google the error message. Yes, we do this too. It helps if you read the error message as well, but unfortunately error messages can frequently be, well, unhelpful. Usually they are too terse, which (after you’ve seen it for the tenth time) becomes a good thing. A google search (or whatever your engine of choice for searching is) can frequently bring you to a discussion of what the error message actually means.

The next simple bit of advice is really hard for people to take. It’s based on the observation that if you ask a class of introductory programming students to rate their own abilities, about 80% of them will rate themselves as being “above average”. This is not true. But it leads people to believe that they can do “simple” things really quickly, when in fact they can’t. It took RMC until half-way through his PhD to write a bug-free program on his first try (it was a short program that used finite differences to solve a PDE, and he did it while a friend was watching, which made it all the sweeter). And this was after two degrees in Computer Science and Mathematics with literally thousands of programs under his belt.

So, what’s the advice? Slow down, and plan it out. Diagram your code in a helpful way, before you touch the keyboard. Write it out on paper, even.

(cue the “Ain’t nobody got time for that” meme, we suppose. Sad trombone to follow.)

Another simple bit of advice (much more likely to be followed) is to develop the habit of starting with small bits. Write a small bit of code that successfully does something.

“I didn’t know how to start, so I did something.”

—One of NJC’s students, at the beginning of a programming assignment; NJC approves of this remark

An important consideration: Use a consistent, legible style. Python enforces indentation, but use it consistently in other languages as well. Use whitespace well and evenly: Chew( a, b ) is much more legible than the asymmetric Chew(a,b ) and likewise line your statements up neatly if you can. Some bugs will just leap out off the page at you when you arrange things neatly.

One more: document your code. The best documentation is accurate and intelligible names for your constructs (variables, programs, data containers). Then use comments. Your future self (who is the one most likely to be reading this code six months from now scratching their head wondering what it was supposed to do) will thank you.

Next most helpful: insert extra print statements when debugging (and delete them afterwards). Print out intermediate results in your code. Or simple messages when certain parts of the code are reached, such as print('Starting the for loop in the next statement') or print('Reached the end of the for loop') or the like. It helps to be verbose there, but if you are in a hurry you can use numbers, \(1\), \(2\), and so on. Delete the print statements afterwards. Some programmers use swearwords, but it’s easy to leave those in, accidentally, when you are finished debugging. So, while possibly cathartic, you might not want to do so. Some people use prayers instead, though there is no documented evidence that this helps.

More professionally, you can use step-by-step debuggers, like pdb. It works in Jupyter notebooks. If you are writing a lot of code, or are building a big project, these can really help. You can use Assert statements. You can use code profilers, which let you know how much computing is done in each piece of code.

But print statements are very simple, and are a big help. Everyone uses them (practically speaking).

Now here is a stupid technique, which works amazingly well:

Rubber Duck Debugging#

Explain your program line by line to a Rubber Duck.

We are not joking

It works. It is ridiculous that it works, but it does. Yes, we use this.

If you don’t have a rubber duck, use a picture of a rubber duck. Or an imaginary rubber duck.

We’re sure that it would work if you used any inanimate object—but then it wouldn’t be real rubber duck debugging.

Now, a more serious-looking tip:

Even for modestly sized projects, use version control#

This book uses git. The book is definitely big enough to need version control of some kind.

Common locations for errors#

  1. At the interfaces: say the procedure is expecting an integer and it’s given a string as input instead. Or, the user of a routine could be expecting a single real number answer and instead the routine delivers a complex number. Or a “list” of things.

  2. The objects have the wrong datatype . This can be obnoxious. Python allows you to be sloppy with datatypes, which is good for getting started, but bad for large projects.

  3. In the documentation: The documentation could say the code was taking the square root of something, and maybe the first version really did; but it since got rewritten and now it takes the cube root instead, maybe, and the documentation never got updated. There is an awful lot of outdated documentation for Python on the web, by the way. Things change.

  4. A simple typo: you typed x1 instead of xl for your variable name just once in the routine. Some programs will catch the use of uninitialized variables, but sometimes it won’t happen. And by the way, try very hard not to use variables named with an l instead of a 1. The symbols are, in some fonts, only a single pixel different on your screen.

  5. A bad range on a loop, or a failure to increment a loop index in a while loop. Counting is hard. (That sounds unsympathetic, but really, it’s inhuman and inhumane to expect people to make no errors when counting.)

  6. Failure to catch the base case of a recursion. Suppose we had only specified \(F_1=1\) and forgotten \(F_0 = 0\) in the Fibonacci code. Then the recursive version would ask for \(F_0\) which would ask for \(F_{-1}+F_{-2}\) and each of these would ask for more negative indices, and maybe make an infinite loop. Actually the code would run out of stack space.

  7. Passing the correct arguments into a procedure but in the wrong order. Maybe the code expects its input to be n (an integer) and x (a real number) in that order and you forget and write Fun(x,n). The code may still run, depending. But you might get a nonsense answer. This is why there is a kwarg way of calling, by the way (“Keyword Argument”). (“Argument” in this context is another word for “input”). You could call Fun(x=3,n=5) or Fun(n=5,x=3) and get the same answer.

  8. An incorrect index into an array or tuple. You might have asked for a[3] but meant to ask for a[2]. This often produces syntax errors, and so is relatively easy to notice, though sometimes very annoying to fix.

  9. Using the wrong version of your code. You have been happily editing one version, but running another. (This is harder to do in Jupyter, since everything is right in front of you, usually).

  10. A transcription error. You might be solving \(F_n = F_{n-1} + F_{n-2}\) but you really wanted to solve \(F_n = F_{n-2} + F_{n-3}\). Your code might be perfect for the first one. This kind of error can be particularly difficult to notice.

  11. A difference in expectation: you might use a routine that someone else wrote, and they expected that the user (this time that’s you) would be calling their routine with (say) small integers, which could fit in the 32 bit integer data type without overflowing. Whereas you, on the other hand, expect their routine to work with long integers. This is what happened in Fibonacci Activity 10. Good documentation can help people to avoid these kinds of errors, but then it’s necessary both for the programmer to write it and the user to read it. You can see the problem. The acronym RTFM, for Read The [Fine] Manual, is frequently seen; but sometimes it doesn’t help.

  12. Using the wrong kind of fences. It is really hard to see that you have written (a,b) when you needed to have written [a,b]. Similarly L(n) means the same thing to a human (more or less) as does L[n], but these are not the same to Python. Incidentally, () are called parentheses or parens, {} are called braces, and [] are called brackets. Then there’s angle brackets (not used in Python as fences) <> which sometimes get called “bras” and “kets” (this is common in physics).

The worst possible kind of errors#

Syntax errors are annoying, because you haven’t even started yet. But actually the interpreter or compiler is trying to help you. It’s much worse when you type legal Python code that does something, but it’s not what you intended (like the typo of 8 for * in the power x*82 when x**2 was intended, in the Schroeder iteration example). Then you would just get wrong answers.

The very worst kind of error is a wrong but plausible answer. One used to say “Garbage In, Garbage Out” but unfortunately too many people simply believe computer output. This gets called “Garbage In, Gospel Out” by people whose cultural background includes the phrase “gospel truth”.

RMC puts it this way: “Probably the single most common error is believing what you see”. We encourage skepticism about the results of computer programs.