Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot

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:

Rows and seats Python · seat_labels.py
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

A grid of seats A1 to C4 numbered 1 to 12 in the order the inner block runs: all of row A first, then row B, then row C.row AA1run 1A2run 2A3run 3A4run 4row BB1run 5B2run 6B3run 7B4run 8row CC1run 9C2run 10C3run 11C4run 12

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:

An inner loop, and the same row as one string Python · stars_by_hand.py
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

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():

Pyramid, inverted pyramid and diamond Python · pyramids.py
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

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:

Floyd's and Pascal's triangles Python · number_triangles.py
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

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.

A multiplication grid Python · times_grid.py
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

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.

Multiplication Table Generator Make a multiplication chart with rows and columns from 1 to 6 to check the grid this program prints.

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:

break leaves only the inner loop Python · free_seats_break.py
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

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:

Stopping both loops Python · free_seats_fixed.py
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

  • Return from a function. return ends the function at once, however deeply the loops are nested; returning None after 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 break there 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:

Making trailing spaces visible Python · hidden_spaces.py
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

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().

Text Diff Paste the pattern your program prints and the one you expected, then compare them by character to see every difference, spaces included. Python Online Compiler Try your own patterns in a full editor, in your browser.

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 i as formulas such as n - i and 2 * i - 1.
  • rjust(), ljust() and center() align text; center() and end=" " leave spaces at the ends of rows.
  • break leaves 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": n rows. Row i (counting from 1) has 2 * i - 1 stars, 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, so 2 * n - 1 rows.
  • "pascal": the first n rows 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 other kind raises ValueError: write raise 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 1

Starter 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.

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.

  1. Question 1 of 5 How many times does the inner block run? for i in range(5): with for j in range(3): inside it.

    Type a number.

    Show the answer to question 1

    Answer: 15 times

    The inner loop runs all 3 of its passes on each of the 5 passes of the outer loop: 5 × 3 = 15.

  2. Question 2 of 5 And with for i in range(1, 6): outside and for j in range(i): inside?

    Type a number.

    Show the answer to question 2

    Answer: 15 times

    The inner loop's size changes from row to row: 1 + 2 + 3 + 4 + 5 = 15, which is 5 × 6 ÷ 2. The same total as the first question, by a different route.

  3. Question 3 of 5 What does seat_labels.py print?

    What does this program print? Choose one answer.

    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")
    Show the answer to question 3

    Answer: it prints

    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

    The outer loop picks a row and the inner loop labels all four of its seats before the next row starts, so each printed line is one row. The counter goes up once per inner pass: 12 for the hall, 10 for the triangle.

  4. Question 4 of 5 Row i of a pyramid (counting from 1) has how many stars?

    Choose one answer.

    Show the answer to question 4

    Answer: 2 * i - 1

    The rows have 1, 3, 5, 7 … stars: two more on each row, starting from 1, which is 2 * i - 1. n - i is the number of spaces in front of them.

  5. Question 5 of 5 A break inside the inner loop of two nested for loops runs. What happens next?

    Choose one answer.

    Show the answer to question 5

    Answer: The outer loop goes on with its next item

    break leaves only the loop it is written in. To stop both, put the loops in a function and return, or set a flag and break the outer loop too, as in free_seats_fixed.py.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.