Python Module 3 – Control flow: decisions, loops and pattern matching
Nested loops and pattern printing
Trace nested loops in Python, count how often the inner block runs, and build star pyramids, diamonds, Floyd's and Pascal's triangles from a formula per row.
What you will learn
- Trace nested loops and count their iterations
- Build star and number patterns row by row from a formula
- Generate Pascal's and Floyd's triangles
Before you start
On this page
A loop’s block can contain another loop. The inner loop then runs from start to finish on every pass of the outer one. That is how programs walk through anything with rows and columns, such as seats in a hall, cells of a table or pixels of an image, and it is the idea behind star and number patterns, a classic way to practise loop control.
How nested loops run
This program labels the seats of a small hall with three rows of four seats, and counts how often the inner block runs:
runs = 0
for row in "ABC":
labels = []
for seat in range(1, 5):
labels.append(row + str(seat))
runs += 1
print(" ".join(labels))
print("The inner block ran", runs, "times: 3 rows x 4 seats")
runs = 0
for row in range(1, 5):
for seat in range(row):
runs += 1
print("A triangle of 4 rows runs it", runs, "times: 1 + 2 + 3 + 4") Output
A1 A2 A3 A4 B1 B2 B3 B4 C1 C2 C3 C4 The inner block ran 12 times: 3 rows x 4 seats A triangle of 4 rows runs it 10 times: 1 + 2 + 3 + 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 seat_labels.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The order in which the inner block of seat_labels.py runs
Text description of the diagram
The diagram is a grid with three rows, A, B and C, and four seats in each row. Each seat is numbered with the run of the inner block that labels it.
- Row A: A1 is run 1, A2 run 2, A3 run 3 and A4 run 4.
- Row B: B1 is run 5, B2 run 6, B3 run 7 and B4 run 8.
- Row C: C1 is run 9, C2 run 10, C3 run 11 and C4 run 12.
The inner loop goes through all four seats of a row before the outer loop moves on to the next row, so the inner block runs 3 × 4 = 12 times in all.
The outer loop takes "A", and the inner loop goes through seats 1 to 4. Only then does the outer loop move on to
"B", and the inner loop starts again from seat 1. So the inner block runs 3 × 4 = 12 times: the number of runs is
the product of the two loop sizes. With 1,000 rows of 1,000 items, that is a million runs, which is worth knowing
before you nest loops over large data.
When the inner loop’s size depends on the outer variable, add up the sizes instead. In the triangle at the end of the
program, row 1 has one pass, row 2 two, and so on, so 4 rows make 1 + 2 + 3 + 4 = 10 runs; n rows make
n × (n + 1) ÷ 2.
One string per row
Patterns made of characters are printed one row at a time. The direct way uses an inner loop that prints one star at a
time, with end="" so that print() does not start a new line after each star:
n = 4
for i in range(1, n + 1):
for j in range(i):
print("*", end="")
print()
for i in range(1, n + 1):
print("*" * i) Output
* ** *** **** * ** *** ****
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stars_by_hand.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both halves print the same triangle. In the second, "*" * i repeats the star i times and does the inner loop’s job
in one expression. Building the row as a string first and printing it once is shorter, and it is easier to check,
because the whole row is one value you can look at. For rows made of several parts, collect them in a list and join
them: " ".join(["1", "2", "3"]) gives "1 2 3".
Work out row i first
A reliable way to write any pattern is to describe row i with a formula before writing a loop. For a pyramid of
n = 4 rows, count the spaces before the stars and the stars on each row:
Row i |
Spaces | Stars |
|---|---|---|
| 1 | 3 | 1 |
| 2 | 2 | 3 |
| 3 | 1 | 5 |
| 4 | 0 | 7 |
The spaces go down by one per row and the stars go up by two, so row i has n - i spaces and 2 * i - 1 stars.
With the formula, every variant is a different range():
n = 4
print("Pyramid")
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))
print("Inverted")
for i in range(n, 0, -1):
print(" " * (n - i) + "*" * (2 * i - 1))
print("Diamond")
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))
for i in range(n - 1, 0, -1):
print(" " * (n - i) + "*" * (2 * i - 1)) Output
Pyramid * *** ***** ******* Inverted ******* ***** *** * Diamond * *** ***** ******* ***** *** *
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pyramids.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The inverted pyramid uses the same formula with i counting down, range(n, 0, -1). The diamond is the pyramid
followed by the inverted pyramid without its first row, range(n - 1, 0, -1), so that the widest row is printed only
once. The left padding is all that centres a row: nothing is printed after the stars.
Number patterns: Floyd’s and Pascal’s triangles
Number patterns follow the same plan, with the numbers worked out in the inner loop:
import math
n = 5
print("Floyd's triangle")
number = 1
for row in range(1, n + 1):
cells = []
for _ in range(row):
cells.append(str(number))
number += 1
print(" ".join(cells))
print("Pascal's triangle")
row = [1]
differences = 0
for i in range(n):
texts = []
for value in row:
texts.append(str(value))
print(" " * (n - 1 - i) + " ".join(texts))
for k in range(len(row)):
if row[k] != math.comb(i, k):
differences += 1
next_row = [1]
for k in range(1, len(row)):
next_row.append(row[k - 1] + row[k])
next_row.append(1)
row = next_row
print("Numbers that differ from math.comb(i, k):", differences) Output
Floyd's triangle
1
2 3
4 5 6
7 8 9 10
11 12 13 14 15
Pascal's triangle
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
Numbers that differ from math.comb(i, k): 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 number_triangles.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Floyd’s triangle puts the counting numbers into rows of 1, 2, 3 … numbers. The counter number is created before
both loops, so it keeps counting from one row to the next. The inner loop’s variable is never used, so it has the
conventional name _.
Pascal’s triangle starts with a row holding a single 1. Each new row begins and ends with 1, and each number in
between is the sum of the two numbers above it, row[k - 1] + row[k]. The numbers are the binomial coefficients, so the
program compares every one of them with math.comb(i, k), the standard library’s count of the ways to choose k of
i items, and finds no difference. The rows line up only while every number has one digit: the sixth row,
1 5 10 10 5 1, would be wider than the spacing allows.
Tables with aligned columns
Numbers of different widths make ragged columns. str.rjust(width) pads a string with spaces on the left up to
width characters, so every column ends in the same place; ljust() pads on the right and center() on both sides.
size = 6
cell = len(str(size * size)) + 1 # the widest product and a space
label = len(str(size)) # the widest row number
line = "x".rjust(label) + " |"
for col in range(1, size + 1):
line += str(col).rjust(cell)
print(line)
print("-" * len(line))
for row in range(1, size + 1):
line = str(row).rjust(label) + " |"
for col in range(1, size + 1):
line += str(row * col).rjust(cell)
print(line) Output
x | 1 2 3 4 5 6 --------------------- 1 | 1 2 3 4 5 6 2 | 2 4 6 8 10 12 3 | 3 6 9 12 15 18 4 | 4 8 12 16 20 24 5 | 5 10 15 20 25 30 6 | 6 12 18 24 30 36
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 times_grid.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The width of a column comes from the widest product, size * size, plus one space to separate the columns, and the
row numbers get the width of the largest one, so the grid stays aligned for any size. Each row is built by its inner
loop and printed once.
Leaving nested loops early
break leaves only the innermost loop. Searches in a grid often need to stop both loops, and this attempt does not:
hall = ["XX.XX", "X..XX", "XXX.."] # X is taken, . is free
# Wanted: the first row with two free seats side by side
for r, row in enumerate(hall):
for s in range(len(row) - 1):
if row[s] == "." and row[s + 1] == ".":
print("Found: row", r, "seats", s, "and", s + 1)
break Output
Found: row 1 seats 1 and 2 Found: row 2 seats 3 and 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 free_seats_break.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The goal was the first row with two free seats side by side, but the program reports two rows. The break ended the
search in row 1, and then the outer loop simply went on to row 2. There are two clear ways to stop everything:
hall = ["XX.XX", "X..XX", "XXX.."] # X is taken, . is free
def first_pair(hall):
for r, row in enumerate(hall):
for s in range(len(row) - 1):
if row[s] == "." and row[s + 1] == ".":
return r, s
return None
print("With return:", first_pair(hall))
found = None
for r, row in enumerate(hall):
for s in range(len(row) - 1):
if row[s] == "." and row[s + 1] == ".":
found = (r, s)
break
if found is not None:
break
print("With a flag:", found) Output
With return: (1, 1) With a flag: (1, 1)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 free_seats_fixed.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- Return from a function.
returnends the function at once, however deeply the loops are nested; returningNoneafter the loops says that nothing was found. This is usually the clearest version. - A flag. Set a variable when the inner loop finds the answer, and test it in the outer loop to
breakthere too.
The else clause of the inner loop can also tell the outer loop that nothing was found, but code written that way is
harder to follow than either version above.
Spaces you cannot see
When a checker compares your output with the expected text, as this lesson’s exercise does, spaces at the end of a
row count. print() does not show them, but repr() does:
n = 3
for i in range(1, n + 1):
print(repr(("*" * (2 * i - 1)).center(2 * n - 1)))
print(repr("*".center(4)), repr("ab".center(5)))
for j in range(1, 4):
print(j, end=" ")
print("|") Output
' * ' ' *** ' '*****' ' * ' ' ab ' 1 2 3 |
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hidden_spaces.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
center() pads both sides, so every row except the widest ends in spaces. When the padding is an odd number of
spaces, one side gets the extra one, and which side depends on the lengths involved: the fourth line of output shows
that "*".center(4) puts one space before the star and two after it, while "ab".center(5) puts two before and one
after.
Printing with end=" " leaves a space after the last number too, just before the |. To get exact rows, build them
with left padding only (" " * (n - i) + ...), join the parts with " ".join(), or remove what is left at the end
with .rstrip().
Key takeaways
- The inner loop runs completely on every pass of the outer loop; the inner block runs (outer size) × (inner size) times, or the sum of the inner sizes when they change from row to row.
- Build each row as one string, with repetition (
"*" * k) or" ".join(parts), and print it once. - Before coding a pattern, write down the spaces and characters of row
ias formulas such asn - iand2 * i - 1. rjust(),ljust()andcenter()align text;center()andend=" "leave spaces at the ends of rows.breakleaves only the inner loop: to stop a nested search, return from a function or use a flag.
Exercise
Exercise · Medium · Python
Return the rows of a pyramid, a diamond or Pascal's triangle
Write pattern(kind, n), which returns the rows of a pattern as a list of strings instead of printing them. kind is "pyramid", "diamond" or "pascal", and n is a whole number, 0 or more. The tests compare every string exactly, spaces included.
"pyramid":nrows. Rowi(counting from 1) has2 * i - 1stars, after just enough spaces to centre it above the bottom row, and nothing after the stars."diamond": the pyramid followed by its rows in reverse order without repeating the widest row, so2 * n - 1rows."pascal": the firstnrows of Pascal's triangle, the numbers of a row separated by single spaces, with no spaces before or after them. Each row starts and ends with 1, and every other number is the sum of the two numbers above it.- For
n = 0, every kind returns[]. Any otherkindraisesValueError: writeraise ValueError(f"unknown kind: {kind}").
These are the rows of pattern("pyramid", 3), of pattern("diamond", 2) and of pattern("pascal", 4), one string per line (the first row of the pyramid is two spaces and a star):
*
***
*****
*
***
*
1
1 1
1 2 1
1 3 3 1Starter code · patterns.py
def pattern(kind, n):
"""Return the n rows of a "pyramid", "diamond" or "pascal" pattern."""
rows = []
return rows The sample tests · test_patterns.py
from patterns import pattern
def test_pyramid():
"""builds a pyramid with no spaces at the ends of its rows"""
assert pattern("pyramid", 1) == ["*"]
assert pattern("pyramid", 3) == [" *", " ***", "*****"]
assert pattern("pyramid", 4) == [" *", " ***", " *****", "*******"]
def test_diamond():
"""puts the pyramid on its mirror image, sharing the widest row"""
assert pattern("diamond", 1) == ["*"]
assert pattern("diamond", 2) == [" *", "***", " *"]
assert pattern("diamond", 3) == [" *", " ***", "*****", " ***", " *"]
def test_pascal():
"""builds the rows of Pascal's triangle"""
assert pattern("pascal", 1) == ["1"]
assert pattern("pascal", 4) == ["1", "1 1", "1 2 1", "1 3 3 1"]
assert pattern("pascal", 6)[-1] == "1 5 10 10 5 1"
def test_zero_rows():
"""returns no rows for n = 0"""
assert pattern("pyramid", 0) == []
assert pattern("diamond", 0) == []
assert pattern("pascal", 0) == []
def test_unknown_kind():
"""raises ValueError for any other kind"""
try:
pattern("square", 3)
except ValueError:
return
assert False, "pattern('square', 3) should raise ValueError" A hint
Write down row i first: for a pyramid of n rows it is n - i spaces and then 2 * i - 1 stars, so a single loop over range(1, n + 1) can append " " * (n - i) + "*" * (2 * i - 1) to the list. For Pascal's triangle, keep the current row as a list of numbers, build the next row from it with an inner loop, and store " ".join(...) of the numbers as text.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Python Tutorial: for Statements (Python Software Foundation)
- The Python Tutorial: break and continue Statements (Python Software Foundation)
- Built-in Types: Common Sequence Operations (Python Software Foundation)
- Built-in Types: str.join (Python Software Foundation)
- Built-in Types: str.center (Python Software Foundation)
- Built-in Types: str.rjust (Python Software Foundation)
- math: math.comb (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress