Why this matters
Debugging means finding out why a program does not do what you meant. It is a skill you use every day as a programmer, and it is easier to learn on small pictures than on large programs.
Most pattern bugs are off-by-one errors. An off-by-one error is a count that is one too big or one too small. It usually sits at the start or the end of a loop. The output looks almost right, and that is why these bugs are easy to miss. You can read the code for ten minutes and still not see a loop that runs once too often.
This lab works through two broken programs. The first prints a diamond with the middle row twice. The second prints a pyramid whose rows have the wrong number of stars. For each one, you will compare the output with the picture you wanted, find the count that is wrong, and fix only that count. The lesson on mirror symmetry uses the same count, n - 1 - i, so the spaces in these programs will look familiar.
A method for finding a bug
Use the same five steps for every broken pattern. The steps do not depend on the shape.
- Draw the picture you want for a small n, such as 3 or 4. Count the rows, and measure the widest row.
- Check the first row and the last row by hand. Most errors show up in these two rows.
- Print the counts before you print the symbols. For each row, print the number of spaces and the number of stars on one line. This is a debug line: a print statement that you add only to see values, and you delete it when the bug is fixed.
- Compare the output with your picture, one line at a time. Note the first line that differs.
- Change one thing, run the program again, and compare again. If you change two things at once, you cannot tell which change fixed the problem.
A few facts about range help here. range(3) gives 0, 1 and 2, so the loop for j in range(3) runs three times. range(4) gives 0, 1, 2 and 3, so the loop for j in range(4) runs four times. Changing range(3) to range(4) adds one more run, which is a common off-by-one error. The stop number is never included in the values.
Lab 1: the broken diamond
The target picture is a diamond with n = 3. A diamond with n rows at the top, and n - 1 rows at the bottom, has 2 * n - 1 rows in all. The widest row is in the middle.
*
***
*****
***
*Here is the broken program. It has a top loop that prints the rows from 0 to n - 1, and a bottom loop that prints the rows again from the bottom up.
n = 3
for i in range(n):
print(" " * (n - 1 - i) + "*" * (2 * i + 1))
for i in range(n - 1, -1, -1):
print(" " * (n - 1 - i) + "*" * (2 * i + 1))Its output is:
*
***
*****
*****
***
*Step 1 is to count the rows. The target has 5 rows and the output has 6, so one row is printed too many. Step 2 is to check the first and last rows. The first row * and the last row * are both correct. The extra row must be in the middle.
Look at the bottom loop. range(n - 1, -1, -1) has three numbers. The first is the start, n - 1. The second is the stop, -1. The third is the step, -1, which means count down by one. The values are 2, 1 and 0 when n = 3. Row 2 is the widest row, and the top loop has already printed it. So the middle row is printed twice. The bottom half must start one row above the middle, at n - 2.
The fix changes one number:
The output is now the target picture:
*
***
*****
***
*The reason for n - 2 is a count. The top half has n rows, from 0 to n - 1, and it includes the middle. The bottom half has n - 1 rows, from n - 2 down to 0. Together they make 2 * n - 1 rows. For n = 3 that is 3 + 2 = 5.
The stop number of a countdown must be -1 to include 0, because range never includes its stop number. A stop of 0 would leave out row 0. The mistakes section below shows that error.
A second example, with n = 4
The same bug at a larger size shows the pattern. This time the bottom loop starts at n - 1 and n = 4. The target has seven rows, and the output has eight:
*
***
*****
*******
*******
*****
***
*The widest row, which has seven stars, is printed twice. The rule is the same: the middle row of a diamond is always the widest row, and the bottom loop must start one row above it. Once you know the rule, you can predict the bug before you run the program. If the output has the widest row twice, the bottom loop starts one row too low.
Lab 2: the unbalanced pyramid
The target picture is a pyramid with n = 4. Each row adds two stars, so the stars are 1, 3, 5 and 7. An odd number of stars has one star in the middle column, so the pyramid sits on the centre line.
*
***
*****
*******Here is the broken program. It prints the spaces with n - 1 - i, which is correct, and it prints the stars with 2 * i, which is not. It also has a debug line that prints the counts for each row. The f before the string in the debug line makes it an f-string, which puts the values of i, spaces and stars into the text.
n = 4
for i in range(n):
spaces = n - 1 - i
stars = 2 * i
print(f"row {i}: spaces={spaces} stars={stars}")
print(" " * spaces + "*" * stars)Its output is:
row 0: spaces=3 stars=0
row 1: spaces=2 stars=2
**
row 2: spaces=1 stars=4
****
row 3: spaces=0 stars=6
******Step 2 is to check the first and last rows. The first row is blank. It has three spaces and no stars, so the top of the pyramid is missing. The last row is ******, which has six stars where the target has seven.
The debug line shows the cause. The star counts are 0, 2, 4 and 6. Every count is even, and each one is one less than the target count for its row. A pyramid row needs an odd number of stars, so that one star sits in the middle column. With an even count, the stars cannot be centred on one column. They sit between two columns, so the row is not centred on the column of the top star.
"*" * stars builds a string of stars stars, which is 2 * i stars here. The target needs 2 * i + 1 stars. The + 1 is the missing count, and it is an off-by-one error in the star count. Change the count to 2 * i + 1, and keep the spaces as they are:
The output shows the counts and the picture together:
row 0: spaces=3 stars=1
*
row 1: spaces=2 stars=3
***
row 2: spaces=1 stars=5
*****
row 3: spaces=0 stars=7
*******Every row now has a star count that is odd, and the stars rise by two per row. A row with an even count in this table would tell you the bug is back.
Once the counts are right, remove the debug line. The same two counts then print the target picture alone:
The output is the target picture:
*
***
*****
*******Mistakes while debugging
These mistakes happen to most beginners. Each one has a clear sign, so you can recognise it.
Changing two counts at once
When the picture is wrong, it is tempting to change the spaces and the stars together. The output may then look right for one value of n, and wrong for another. Change one count at a time, run the program with n = 3 and with n = 4, and then decide whether the change worked.
Adding spaces to fix the picture
If the top row does not line up, it is tempting to add a space to the top row by hand. That moves one row but does not fix the loop. The next run for a different n will be wrong again. Find the count that is wrong, and fix the line that prints it.
Using a loop variable after the loop
In Java, a variable declared in a for loop exists only inside that loop, and using it after the loop is a compile error. Python does not work that way. A loop variable stays in place after the loop, with the last value the loop gave it:
The output is:
*
*
*
2The value printed is 2, the last value of i. A program that uses this value by accident runs without an error, so the mistake is harder to spot. Read the loop before you trust a value that is used after it.
If the loop never runs, the name is never created at all:
for k in range(0):
pass
print(k)The program prints:
Traceback (most recent call last):
File "/path/to/file.py", line 3, in <module>
print(k)
^
NameError: name 'k' is not definedThe folder part of the path depends on where you save the file. A NameError means Python does not know a name. The fix is to give the variable a value before the loop, such as k = 0, if you need it after a loop that may not run.
Stopping the countdown at 1
This is the bottom loop from Lab 1 with the stop number set to 0 instead of -1:
The output is:
*****
***Row 0 is missing. range stops before its stop number, so the value 0 is never reached. The stop for a countdown that ends at 0 is -1, which is why the fix uses range(n - 2, -1, -1).
A count that goes negative
Off-by-one errors in the space count can make the count negative. In Java, a string's repeat method refuses a negative count and stops the program. Python is quieter. A string repeated with a negative count is an empty string, and no error is raised. This program uses range(n + 1), so the loop runs one time too many:
The repr function shows a value the way Python writes it, so an empty string appears as two quote marks. The output is:
*
*
*
*
''The fourth row has a count of -1. " " * -1 gives an empty string, so its star lands at column 0 and the row looks like a real row. No error stops the program, so the only sign of the bug is the picture. Compare every row with your target. The fix is range(n), not range(n + 1).
Practice
Exercise 1
The diamond bug from Lab 1 is in a program with n = 4. The bottom loop starts at n - 1. Which row prints twice, and how many stars does it have?
Show the answer
The widest row prints twice. For n = 4, the widest row is row 3, which has 2 * 3 + 1 stars, so seven stars. The output has two lines of seven stars in the middle, as shown in the second example of Lab 1.
Exercise 2
Is this loop correct for a pyramid? Answer yes or no, and explain why.
for j in range(2 * i + 2):
print("*", end="")Show the answer
No. range(2 * i + 2) gives 2 * i + 2 values. For row 0 it prints two stars, and for row 1 it prints four. A pyramid row needs one star in row 0 and three in row 1, so the loop needs range(2 * i + 1).
Exercise 3
A pyramid with n = 6 needs five spaces in its top row. Write the loop for the spaces, so that the top row gets exactly five spaces. Then check the bottom row, which should have none.
Show the answer
The loop is for _ in range(n - 1 - i):. In row 0, i is 0, so n - 1 - i is 5. list(range(5)) is [0, 1, 2, 3, 4], five values, so the loop runs five times. In the bottom row, i is 5, so n - 1 - i is 0. range(0) gives no values, so the loop does not run. Both rows match the target.
Exercise 4
What does " " * -1 do in Python? Write the result, and say whether an exception is raised.
Show the answer
It returns an empty string, which repr shows as ''. No exception is raised, and the program keeps running. This is why the picture must be checked by eye. The fix is still to make sure the count can never be negative.
Exercise 5
Predict the output of the fixed diamond when n = 2. The program has the same two loops as Lab 1, with the bottom loop starting at n - 2. Write each line, including the spaces.
Show the answer
With n = 2 the top loop prints two rows and the bottom loop prints one row. The output is:
*
***
*The top row has one space and one star. The second row has no spaces and three stars. The bottom loop starts at n - 2, which is 0, so it prints the top row again as the last row. This matches the picture of a diamond with 2 * 2 - 1 = 3 rows.
Summary
- Draw the target picture for a small n, count the rows and measure the widest row before you change any code.
- Check the first and last rows by hand. Most off-by-one errors show up there.
- Print the counts for each row with a debug line, then compare them with the picture.
- A diamond with n rows at the top has 2 * n - 1 rows in all, and its bottom loop uses
range(n - 2, -1, -1), notrange(n - 1, -1, -1). - A pyramid row needs an odd number of stars, so the star count must be
2 * i + 1. - Change one count at a time, and test with two or more values of n before you decide the fix works.