Why this matters
A pattern is a picture made of text. Programmers print patterns to test a loop, to draw a banner in a terminal, or to practise planning code before writing it. The earlier lessons in this module printed squares, triangles, pyramids and diamonds. This lesson adds three shapes that change width in a different way.
An hourglass starts wide, gets narrow in the middle, and gets wide again. An arrow is a triangle sitting on top of a narrow stem. A cross is a thin vertical bar with a wide bar across its middle. Each of these shapes can be printed with one or two loops and a width value. The main skill is working out how the width changes from one row to the next. Once you can say that in words, the code is short.
The diamond has its own lesson, and Pascal's triangle comes next, so this lesson does not repeat either of them.
The idea: a width, an indent and a step
Every row of a text picture has two numbers. The width is how many stars the row contains. The indent is how many spaces come before the stars. If you know both numbers for every row, you can print any shape in this lesson.
Start with a small hourglass. It has five stars at the top, three in the next row, one in the middle, then three and five again. Written out, it looks like this:
*****
***
*
***
*****Read the widths from top to bottom: 5, 3, 1, 3, 5. Each row differs from the next by 2. That fixed difference is the step. On the way down the width goes down by the step until it reaches the narrowest row. On the way back up it goes up by the same step until it reaches the widest row again.
Now read the indents from top to bottom: 0, 1, 2, 1, 0. The shape is centred, so each row starts with exactly the number of spaces that puts it in the middle. The widest row has no spaces in front. Each row that is two stars narrower has one more space on each side. Because the width changes by 2, the indent changes by 1.
The indent can be written as a formula. It is the widest width minus this row's width, divided by 2:
indent = (widest - width) // 2The symbol // is whole-number division. It drops any fraction. The single slash / in Python gives a decimal number instead, even when the answer is whole. The next section shows why this matters.
For the row with width 3 in a shape whose widest row is 5, the indent is (5 - 3) // 2, which is 1. The division by 2 is needed because the space is taken from both sides. Half of the difference goes on the left and half on the right. Because the difference is always even here, the result is always a whole number.
Python builds a string by repeating text with the * operator. The expression " " * 2 gives two spaces. The expression "*" * 3 gives three stars. You do not need an inner loop just to print spaces.
The range function makes the sequence of numbers for a loop. The call range(5, 0, -2) gives 5, 3, 1. It starts at 5, adds -2 each time, and stops before 0. The stop value is never included. The call range(3, 6, 2) gives 3 and 5. The examples in this lesson were run with Python 3.14.4.
Worked example 1: an hourglass
The program below prints the hourglass from the last section. It uses a helper function called row. A function is a named block of code that you can call by its name. The helper takes two numbers, the width of the row and the width of the widest row. It returns a string, which is the text of one row. It does not print anything itself. The main function prints the rows.
The def keyword starts a function. The lines under it are indented, and Python uses the indent to know where the function ends. There are no braces. The return line gives the result back to the caller.
The output is:
*****
***
*
***
*****The first loop starts at w = widest, which is 5. The range call range(widest, 0, -2) gives 5, 3 and 1. The stop value is 0, so the loop ends after the value 1.
The second loop starts at 3, not 1. The narrowest row, the single star, was already printed by the first loop. If the second loop started at 1, that star would appear twice and the middle of the hourglass would look thick. The stop value is widest + 1, which is 6. That makes the loop include 5, because the stop value itself is never included.
The table traces every call to row:
| Loop | Width w | Indent (5 - w) // 2 | Printed row |
|---|---|---|---|
| first | 5 | 0 | ***** |
| first | 3 | 1 | *** |
| first | 1 | 2 | * |
| second | 3 | 1 | *** |
| second | 5 | 0 | ***** |
Five calls, five rows. The widths and indents match the picture in the last section.
Worked example 2: an arrow
An arrow has two parts. The head is a triangle that grows from one star to the widest row. The stem is a narrow column that keeps the same width for every row. The first part needs a width that grows by a step. The second part needs a width that does not change. That makes the arrow a good example of two loops that do different jobs.
The head is built with w starting at 1 and going up by 2 until it reaches the widest row. With a widest row of 5, the head has three rows: 1, 3 and 5 stars. The stem has stem_height rows, and each row is one star wide. Both parts use the same row function, so the stem is centred under the head without any extra work.
The output is:
*
***
*****
*
*
*The head rows have widths 1, 3 and 5, so their indents are 2, 1 and 0. The stem row has width 1, so its indent is (5 - 1) // 2, which is 2. The stem lines up with the tip of the head. If you change widest to 7, the head becomes four rows tall, and the stem moves one space to the right so that it stays under the tip. No other change to the code is needed.
Notice what the second loop does not do. It does not change w. It uses the name _ for its counter. In Python, _ is a common name for a value that the loop does not use. It only counts the rows. This is a useful habit. When a part of a shape keeps the same width, use a plain counter. When the width changes, change the width with a step.
Worked example 3: a cross
A cross is a different case. Its width does not grow by a step. It jumps from 1 to the full size, stays there for one row, and then goes back to 1. You can still use the same row function. The only question is which row gets the full width.
The bar across the middle is on the row whose number is size // 2. With a size of 5, the middle row number is 2, so the bar is the third row from the top. There are two rows above it and two rows below it. The code checks each row number with a conditional expression. This is a short way to pick one of two values. The form is first_value if condition else second_value.
The output is:
*
*
*****
*
*The conditional expression reads as "if the condition is true, use the first value, otherwise use the second value". Here, the condition is r == middle. The loop variable r counts rows from 0. Rows 0, 1, 3 and 4 get width 1. Row 2 gets the full width of 5.
This program has one loop, not two, because the cross has no step to follow. The width does not decrease and then increase. That is why the cross is useful to study. It shows that the two loops are not a rule. The rule is to look at how the width changes, then pick the loop that matches that change.
The size should be odd. With an odd size such as 5 or 7 there is exactly one middle row, and the cross is symmetrical. With an even size such as 4, the middle row number is 2, which is half a row below the true centre, so the bar sits off-centre. You can check this by running the program with size = 4.
Worked example 4: one loop that turns around
The hourglass in the first example used two loops. There is another way to print the same shape with one loop. The loop keeps a variable step. The step is -2 while the width is falling and +2 while it is rising. When the width reaches 1, the program flips the step, so the next width goes up.
The output is the same hourglass as before:
*****
***
*
***
*****The loop runs five times, once for each row. Each time, it prints the current width, then checks whether it has reached the narrowest row. If the width is 1, the step turns from -2 into +2. Then it adds the step to the width. The next row is the one after the turn. The expression width += step is a short way to write width = width + step.
This version is harder to read than two loops. For a beginner, two loops are usually easier to check. The one-loop version matters when the number of rows is the thing you need to control, for example when a shape must print exactly seven rows no matter what the widths are. In that case, the loop counter decides when to stop, and the step decides how wide each row is.
Common mistakes
Mistake 1: an even widest width
The hourglass only looks right when the widest row is odd. Here is the same program as the first example, with widest set to 4:
The output is:
****
**
***The first loop goes 4, 2, and then stops because 0 is not in the range. The second loop starts at 3 and goes up to 4, so it prints one row of width 3. The shape goes 4, 2, then 3, and it has no single-star row, so it is not a diamond or an hourglass at all. The program ran without an error, so this kind of mistake is easy to miss. The fix is to think about which widths the step can reach. With a step of 2, a widest row that is even can never reach 1. Use odd widths for hourglasses, arrows and crosses in this lesson.
Mistake 2: a width larger than the widest row
If a width is bigger than widest, the indent becomes negative. In many languages a negative repeat count stops the program with an error. Python does not. Repeating text a negative number of times gives an empty string, so the program keeps going and prints a wrong shape. Here is a short program that calls row with width 7 and widest 5:
def row(width, widest):
indent = (widest - width) // 2
return " " * indent + "*" * width
print(repr(row(7, 5)))The output is:
'*******'The indent is (5 - 7) // 2, which is -1. The expression " " * -1 is an empty string, so the row has no spaces in front and seven stars. The row is wider than the widest row, and no error is shown. The repr function in the example shows the value between quotes, so you can see that the string has no leading spaces. The fix is to keep every width at or below widest. In a shape built from a loop, check the loop bounds first.
Mistake 3: a loop that never changes the width
A loop that prints a shape needs a step that changes the width. If you write a while loop and forget the step, the value of width never changes. The test width >= 1 stays true, so the loop never ends:
width = 5
while width >= 1:
print(row(width, 5))The program prints the same row again and again until you stop it. In a terminal, press Ctrl+C to stop it. The fix is to write the step inside the loop, next to the line that prints, where it is easy to see. A for loop with range avoids this mistake, because the step is part of the loop header.
Mistake 4: a name that is not defined
If you call row from a place where the function is not defined, Python stops with a message. For example, this program calls row but the function is not in the file:
widest = 5
for w in range(widest, 0, -2):
print(row(w, widest))Python reports this when it runs the line. The path in the first line depends on where the file is saved. Here it is shown as main.py:
Traceback (most recent call last):
File "main.py", line 3, in <module>
print(row(w, widest))
^^^
NameError: name 'row' is not defined. Did you mean: 'pow'?The last line names the problem. A NameError means Python cannot find a name, here row. The line Did you mean: 'pow'? is a guess from the names that do exist. The fix is to define row with def, before the loop runs.
Mistake 5: a missing colon
Every def, for, while and if line ends with a colon. Python reports a missing colon before it runs anything. Here is a function header with the colon missing:
def main()
print("hourglass")Python reports:
File "main.py", line 1
def main()
^
SyntaxError: expected ':'The caret points to the place just after the closing bracket, where the colon should be. The fix is to add the colon at the end of the header line. A Python file with a syntax error does not run at all, not even the lines before the error.
Practice
Exercise 1: a wider hourglass
Change the first hourglass program so that widest is 7. Predict the output before you run it. Then check your prediction.
Show the answer
Keep the same two loops and the same row function. Only the value of widest changes. The first loop prints widths 7, 5, 3 and 1. The second loop prints 3, 5 and 7. The output has seven rows:
*******
*****
***
*
***
*****
*******The indent of the widest row is 0, and the narrowest row has an indent of 3, which is (7 - 1) // 2.
Exercise 2: an arrow that points down
Write a program that prints an arrow with the head at the bottom. The stem comes first, three rows of one star. Then the head goes from the widest row down to one star. Use widest set to 5.
Show the answer
The stem uses a counter loop, and the head uses a loop that goes down by 2:
The output is:
*
*
*
*****
***
*Exercise 3: a cross of size 7
Print a cross with size set to 7. The bar should be on the middle row. Predict how many spaces come before the single star on the top row.
Show the answer
The middle row number is 7 // 2, which is 3. The top row has width 1, so its indent is (7 - 1) // 2, which is 3. There are three spaces before the star. The cross program from worked example 3, with size = 7, prints:
*
*
*
*******
*
*
*Exercise 4: why the even width fails
Change the hourglass to widest = 4. Without running it, say which widths the first loop prints and why the output has no single-star row. Then run the program and compare.
Show the answer
The first loop starts at 4 and goes down by 2. The values in the range are 4, then 2, then 0. The value 0 is the stop value, so it is not included. The loop prints widths 4 and 2 and stops. The loop never reaches 1, so there is no single-star row. The second loop starts at 3 and goes up by 2, so it prints only the width 3, and the shape is 4, 2, 3.
Whether a number is odd or even is called its parity. The first loop uses even numbers and the second uses odd numbers, so the two halves do not meet at a single centre row. The real output is:
****
**
***The fix is to use an odd widest.
Summary
- A row of a text shape has a width (the number of stars) and an indent (the number of spaces in front).
- In a centred shape, the indent is
(widest - width) // 2. - A width that changes by a fixed step gives a clear hourglass or arrow head, using two loops whose step goes down and then up.
- A part of a shape that keeps the same width needs only a counter, not a changing width.
- Odd widths keep the middle row centred. An even widest width gives a shape with no single centre star.
- Python does not stop on a negative repeat count. Check the widths yourself, and read the last line of a Python error first, since it names the problem.