Why this matters
The earlier lessons in this module printed each cell as the loop reached it. A star, a number or a space went to the screen with print(..., end=""), and a plain print() moved the cursor to the next line at the end of the row. That works, and it is the right tool for many patterns.
There is a second way. You can collect all the characters of one row into a single piece of text, then print that text with one call. The row is then a value in the program. You can count its characters, change it, or check it before anything appears on the screen.
This lesson compares the two ways on the same pattern, then shows the tools Python gives you for building rows: lists, the join method and the * operator for repeating text. The Aligned tables lesson, which comes later in this module, uses the same tools to line up columns of different widths. This lesson does not cover that.
Terms used here
A string is a piece of text in Python, such as "abc". A string cannot be changed after it is made. Methods such as replace and upper return a new string and leave the old one alone.
A list is an ordered collection of values, written with square brackets, such as ["a", "b"]. A list can be changed. append adds one item to the end, and clear removes every item. In this lesson a list holds the pieces of one row until they are joined.
The length of a string or a list is the number of items in it. len("abc") gives 3. A space counts as one character.
Python has no separate type for a single character. A single character is a string of length 1, such as "*".
join is a method of a separator string. " ".join(["1", "2"]) puts a space between the items and gives "1 2". Every item must already be a string.
An f-string is a string with an f before the opening quote. Each part in braces is replaced by the value of the expression inside the braces. f"row {row}" gives row 3 when row is 3.
The idea from first principles
Here is a pattern of four rows. Row r has r stars.
*
**
***
****There are two ways to print it.
The first way prints each star as the loop reaches it, and moves to a new line at the end of each row:
for row in range(1, 5):
for _ in range(row):
print("*", end="")
print()The second way builds each row in a list, then joins the list into one string and prints the whole row with one call:
for row in range(1, 5):
line = []
for _ in range(row):
line.append("*")
print("".join(line))Both programs print the same four lines. The output is:
*
**
***
****The difference is in what the program does between the first star and the screen. In the first version, each star goes straight to the output. In the second, the stars are stored in line first, and the row goes to the output when join runs. For four rows, the first version calls print 14 times: 10 times for the stars and 4 times for the ends of rows. The second version calls print only 4 times, because the 10 append calls only change the list in memory.
Some people build a string with line += "*" instead. That also works, because += joins two strings. This lesson uses the list and join pattern, because it is the usual way to build one string from many small pieces.
The output looks identical, so the choice depends on what you need to do with the row. If the row must be measured, changed or checked before it is shown, it has to exist as a value. If each cell is printed exactly as it is computed, the cell version is shorter.
Worked example 1: inspect a row before printing it
The main reason to build a row is that you can look at it first. This program builds each row, then prints the row and its length. The length is only known because the row exists as a value.
The output is:
* (1 stars)
** (2 stars)
*** (3 stars)
**** (4 stars)Trace row 3. line starts as an empty list. The inner loop runs three times, and each time append("*") adds one star to the end, so line holds three items. "".join(line) joins them into the string ***, and text holds that string. Then len(text) returns 3, which is the number of stars.
The same length check is hard to do in the cell version. Once a star has been printed, the program has no record of how many were printed on that row, unless it keeps its own counter.
Before you move on, look at one detail. line = [] is inside the outer loop, so each row starts with an empty list. If it were created before the outer loop, the stars from row 1 would still be there in row 2. The common mistakes section shows what happens when a list is not cleared.
Worked example 2: a bar for each score
The second example uses the * operator on strings. "#" * 3 gives "###". "#" * 0 gives an empty string, "". The * operator repeats a string by a whole number, and it works the same way in every version of Python 3.
The program prints one bar for each score. The length of the bar is the score, so the bar is built with one expression instead of a loop. The zip function pairs the items of two lists, so the first name goes with the first score, the second name with the second score, and so on.
The output is:
Ana | ####### 7
Ben | ### 3
Chitra | ########## 10
Dev | 0The row for Dev is built from an empty bar, so the line has two spaces after the bar mark. The row still exists as a value, so the program does not need a special case for a score of zero. The * operator returns an empty string, and the rest of the line is built the same way.
The names are not padded, so the bars do not line up. Making the bars start at the same column is the job of the Aligned tables lesson. Here the goal is only to show that a row can be assembled from named parts.
Worked example 3: join a list of cells
The third example builds a row from a list. Each cell is converted to a string and added to a list. Then join puts the list together with a separator between each pair of items.
The output is:
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15The line [str(row * col) for col in range(1, 6)] is a list comprehension. It builds a list in one line. For each col, it computes str(row * col) and puts the result in the list. The same list can be built with three lines: create an empty list, loop over the columns, and call append for each cell.
str turns a number into text. The list holds text, not numbers, so join can use it. The join step runs once per row, after all the cells are known. That gives the row one place where its separator is set. If the separator must change from a space to a comma, only the " " in one line changes.
The list also makes the row easy to change before printing. For example, you could remove the first cell, or replace every cell that is a multiple of 5 with a marker, and the join would still work. The cell version can do the same work, but the change has to be made in the middle of the printing loop.
This example prints numbers that have different widths, such as 8 and 10 in row 2. The columns do not line up on the screen. That is expected for this lesson.
The samples in this lesson were run with Python 3.14.
Common mistakes
A string method call that does not change the string
replace, upper and strip return a new string. They do not change the one they are called on. If the result is not stored, the change is lost. Python does not report an error, because the call itself is valid.
The output is:
* * *The spaces are still there. The fix is to store the result:
The output is:
*..*..*Forgetting to clear a list
A list keeps its items until you remove them. If it is reused for the next row without being cleared, the new row starts with the old one.
The output is:
#
##
###Each row keeps the items from the row before. The fix is line.clear() at the end of each row, which removes all the items. With that line added, the same program prints # three times:
The output is:
#
#
#Creating a new empty list inside the outer loop does the same job, as shown in worked example 1.
Joining text with a number
Python does not turn a number into text when you join it with + or with join. Both of the next two programs stop with a TypeError. A TypeError is an error that means a value has the wrong type for the operation.
row = 3
print("row " + row)Running this file gives:
Traceback (most recent call last):
File "<folder>/main.py", line 2, in <module>
print("row " + row)
~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to strThe message names the two types. str is a string, and int is a whole number. The fix is to convert the number first with str(row), or to use an f-string: print(f"row {row}").
print(", ".join([1, 2, 3]))Running this file gives:
Traceback (most recent call last):
File "<folder>/main.py", line 1, in <module>
print(", ".join([1, 2, 3]))
~~~~~~~~~^^^^^^^^^^^
TypeError: sequence item 0: expected str instance, int foundThe message says that item 0 of the list is an int, and join needs a str. The fix is ", ".join([str(n) for n in [1, 2, 3]]).
The path in the first line of each traceback depends on where the file is saved.
A negative count gives an empty string
In some languages, repeating text by a negative count stops the program with an error. In Python, "*" * -1 gives an empty string and no error is shown. A loop that counts down past zero therefore prints empty lines, and the bug can go unnoticed.
The output is:
'***'
'**'
'*'
''
''The repr function shows a value the way Python writes it, so an empty string appears as two quote marks. A loop that counts down needs a stop condition, such as row >= 0, or a check before the repeat.
Practice
Exercise 1: A centered pyramid
Print a pyramid of stars with 4 rows. Row 1 has one star, and row 4 has seven stars. Each row is centered, so the spaces before the stars shrink as the rows get longer. Build each row with * and print it once.
Show the answer
Row row has 4 - row spaces and 2 * row - 1 stars.
The output is:
*
***
*****
*******Exercise 2: Floyd rows joined once
Print the first four rows of Floyd's triangle, using a list of cells for each row and join to print it. The first row holds 1, the second holds 2 and 3, and so on. The counter must not reset between rows.
Show the answer
The output is:
1
2 3
4 5 6
7 8 9 10Exercise 3: Cells or rows
Print a 3 by 3 block of x characters twice. The first block is built as one row string per line. The second block is printed one character at a time. Put a line of three dashes between the two blocks. Then run the program and check that the two blocks match.
Show the answer
The output is:
xxx
xxx
xxx
---
xxx
xxx
xxxThe first block builds a row string with x and prints it. The second block prints each character directly. Both blocks match.
Exercise 4: Numbered uppercase lines
Given the list ["alpha", "beta", "gamma"], print each word in uppercase, with its position counted from 1 and the length of the finished line in brackets. The position must start at 1, not 0.
Show the answer
enumerate gives each item with its position. start=1 makes the position begin at 1 instead of 0. The f-string builds the whole line first, so len(line) counts all of it, including the number and the colon.
The output is:
1: ALPHA (8 characters)
2: BETA (7 characters)
3: GAMMA (8 characters)Exercise 5: A comma list with no trailing comma
Build the text 1,2,3,4,5 in one string. Put a comma after every number except the last. Print the text, then print its length.
Show the answer
join puts the separator only between items, so the last number gets no comma and no check is needed. The list comprehension builds the list of strings first.
The output is:
1,2,3,4,5
length: 9The text has 5 digits and 4 commas, so its length is 9.
Summary
- Printing cell by cell and building the row first give the same output, but building the row makes the row a value that can be measured or changed.
- A list collects pieces with
append,"".join(line)turns them into one string, andline.clear()empties the list before the next row. - The
*operator repeats a string, and a count of zero gives an empty string. joinputs a list of strings together with one separator, which can be changed in one place.- String methods such as
replacereturn a new string, so the result must be stored.