第 03 章
循环:让重复变成结构
CS50P 第 2 周:用 while 与 for 把重复劳动变成循环,理解计数、无限循环与输入校验,再借列表、字典与嵌套循环写出行列与方块,最后用函数把问题层层抽象。
Loops: Turning Repetition into Structure
CS50P week 2: replace repeated code with while and for loops, learn counting, infinite loops and input validation, traverse lists and dictionaries, build rows and squares with nested loops, and abstract the problem into functions.
篇内目录
- What this chapter covers
- Timeline
- The problem: repeated code
- while: managing the counter yourself
- Infinite loops
- Counting up, down and +=
- for: letting Python count
- range
- Naming an unused variable _
- String multiplication
- Input validation: while True + break
- Wrapping it in functions
- Lists: several values in one variable
- Traversal
- When you need indices
- Dictionaries: key-value pairs
- A list of dictionaries
- Mario: building with loops and abstraction
- A column
- A row
- A square
- Simplifying with string multiplication
- The value of abstraction
- Chapter summary
- Comprehension check
- Relation to neighbouring chapters
这一章在讲什么
这是长视频的第 3 章,对应 CS50P 第 2 周(02:46:38–04:07:34),主题是循环。
起点是一个很小的毛病:让猫叫三声,最直接的办法是写三行 print("meow")。能跑,但一旦要叫五十声、五百声,或者把「meow」换成「woof」,代码就崩了。循环正是为了解决「同一件事重复做」这一整类问题。
本章按这条线展开:先用 while 手工管理计数器,踩一次无限循环的坑;再用 for 与 range 把计数交给 Python;然后用 while True 加 break 做输入校验;接着引入列表与字典,让循环能遍历数据而不只是数字;最后用马里奥的砖块例子,把循环、函数与抽象组合起来。
时间轴
| 时间 | 内容 |
|---|---|
| 2:46:38 | 本周主题:为什么要循环 |
| 2:47:00 | cat.py:三行 print 的问题 |
| 2:49:56 | while 循环与计数器 |
| 2:52:06 | 无限循环:为什么危险,怎么用 Ctrl-C 停下 |
| 2:54:11 | 递减计数,程序终于会停 |
| 2:56:26 | 改成递增、从 1 数到 3 |
| 2:58:15 | 从 0 开始计数的惯例与 i += 1 |
| 3:02:43 | for 循环与列表 [0, 1, 2] |
| 3:07:16 | range(3):不为极端情况手工列值 |
| 3:09:04 | 用 _ 表示「不关心的循环变量」 |
| 3:09:35 | 字符串乘法 "meow\n" * 3 |
| 3:13:11 | 让用户决定叫几声 |
| 3:14:44 | while True + continue / break 做输入校验 |
| 3:18:05 | 把 meow 与 get_number 封装成函数 |
| 3:21:06 | 列表:students = ["Hermione", "Harry", "Ron"] |
| 3:23:04 | 索引从 0 开始;students[0] |
| 3:24:03 | for student in students 遍历列表 |
| 3:27:26 | len() 与 range(len(students)) |
| 3:32:00 | 字典:键值对与花括号 |
| 3:38:15 | 遍历字典得到的是键 |
| 3:43:18 | 列表套字典:每个学生多个属性 |
| 3:46:00 | None:值的缺席 |
| 3:51:28 | 马里奥:打印一列、一行、一个方块 |
| 4:01:55 | 嵌套循环与 print() 换行 |
| 4:05:03 | 用字符串乘法简化内层循环 |
问题:重复的代码
print("meow")
print("meow")
print("meow")
三段一模一样的代码。问题不在「三行」,而在它的延伸:
- 要叫三百声怎么办?复制粘贴三百行?
- 想把猫换成狗,得改三百处;
- 想根据用户输入决定次数,根本做不到。
循环把「重复」从写多少行变成一个参数。
while:手工管理计数器
while 后面跟一个布尔表达式,只要它为真,就反复执行缩进块。
i = 3
while i != 0:
print("meow")
i = i - 1
输出三行 meow。执行顺序是:
i = 3;- 判断
i != 0,为真; - 打印,
i变成 2; - 回到第 2 步,直到
i变成 0,判断为假,退出。
无限循环
如果忘了第 3 步的更新:
i = 3
while i != 0:
print("meow")
i 永远是 3,条件永远为真,程序会一直打印下去。这类事故叫无限循环。在终端里遇到它,按 Ctrl-C 即可中断,不需要重启电脑。
补充: 这是
while最典型的错误。写while时要立刻问自己:循环体里有没有改变条件的那一步?
递增、递减与 +=
从 1 数到 3:
i = 1
while i <= 3:
print("meow")
i = i + 1
大多数程序员习惯从 0 开始数,因为列表索引从 0 开始:
i = 0
while i < 3:
print("meow")
i += 1
注意这里是 < 3 而不是 <= 3:从 0 开始、到 3 之前停止,正好三次。
i += 1 是 i = i + 1 的简写。Python 没有 i++——那是 C、C++、Java 的写法。
for:把计数交给 Python
for i in [0, 1, 2]:
print("meow")
for 会依次把列表里的每个值赋给 i,每赋一次执行一遍循环体。计数器不用你初始化、不用你更新,i 自动变成 0、1、2。
但手工写出 [0, 1, 2] 又回到了老问题:要叫一百万声呢?
range
for i in range(3):
print("meow")
range(n) 返回从 0 开始、到 n 之前(不含 n)的整数序列。range(3) 就是 0, 1, 2。
要叫一百万声,只需 range(1_000_000)。
不用的变量叫 _
循环变量如果根本不用,Python 的惯例是命名成单个下划线:
for _ in range(3):
print("meow")
这告诉读代码的人:这里需要一个计数器,但我不关心它的值。不要明明在用却叫它 _——那只会让人困惑。
字符串乘法
Python 允许用 * 重复字符串:
print("meow\n" * 3, end="")
输出三行 meow。"meow\n" 是一个带换行的单元,乘以 3 得到三个单元;end="" 阻止 print 在末尾再加一个换行,避免多出空行。
这行代码很短,但可读性不如循环。课程的态度是:能用,但别为了炫技牺牲可读性。
输入校验:while True + break
让用户决定叫几声,而且要保证输入是正数。直接 if 判断一次不够——用户可能连续输错。用循环:
while True:
n = int(input("What's n? "))
if n > 0:
break
while True 是故意制造无限循环,靠 break 跳出。执行流程:
- 条件恒真,进入循环;
- 读取输入;
- 若
n > 0,break跳出; - 否则什么都不做,回到第 1 步再问一次。
如果只想跳过本次、继续下一轮,用 continue:
while True:
n = int(input("What's n? "))
if n < 0:
continue
break
封装成函数
def main():
number = get_number()
meow(number)
def get_number():
while True:
n = int(input("What's n? "))
if n > 0:
return n
def meow(n):
for _ in range(n):
print("meow")
main()
两个细节:
get_number用return n直接结束函数并交回值,比break之后再return更简洁;meow只负责打印,get_number只负责取值,职责分开。
列表:把多个值放进一个变量
students = ["Hermione", "Harry", "Ron"]
方括号表示列表。访问元素用方括号加索引,索引从 0 开始:
print(students[0]) # Hermione
print(students[1]) # Harry
print(students[2]) # Ron
遍历
for student in students:
print(student)
不需要知道列表有多长,也不需要索引。变量名用 student 而不是 s 或 _,因为它确实代表一个学生。
需要索引时
for i in range(len(students)):
print(i + 1, students[i])
len(students) 返回列表长度,range(len(...)) 生成索引序列。i + 1 只是为了显示时从 1 开始。
字典:键值对
列表用数字索引,字典用你选的键索引:
students = {
"Hermione": "Gryffindor",
"Harry": "Gryffindor",
"Ron": "Gryffindor",
"Draco": "Slytherin",
}
print(students["Hermione"]) # Gryffindor
花括号表示字典,键: 值 成对出现。查找时把键放进方括号。
遍历字典:
for student in students:
print(student, students[student], sep=", ")
for 遍历字典时,拿到的是键(不是值)。输出:
Hermione, Gryffindor
Harry, Gryffindor
Ron, Gryffindor
Draco, Slytherin
列表套字典
一个学生有多个属性时:
students = [
{"name": "Hermione", "house": "Gryffindor", "patronus": "Otter"},
{"name": "Harry", "house": "Gryffindor", "patronus": "Stag"},
{"name": "Ron", "house": "Gryffindor", "patronus": "Jack Russell terrier"},
{"name": "Draco", "house": "Slytherin", "patronus": None},
]
for student in students:
print(student["name"], student["house"], student["patronus"], sep=", ")
None 是 Python 表示「没有值」的关键字,首字母大写。Draco 没有守护神,用 None 比用空字符串 "" 更明确——空字符串可能意味着「忘了填」,None 表示「确实没有」。
马里奥:用循环与抽象搭方块
一列砖
def main():
print_column(3)
def print_column(height):
for _ in range(height):
print("#")
main()
想要更高的柱子,改一个参数即可。
一行砖
def print_row(width):
print("?" * width)
字符串乘法让「打印一行」变成一行代码。
一个方块
def print_square(size):
for i in range(size):
for j in range(size):
print("#", end="")
print()
输出:
###
###
###
要点:
- 外层循环负责行,内层循环负责一行里的砖;
- 内层用
end="",让砖块不要换行,排在同一行; - 内层结束后调用一次
print(),它只输出一个换行,把光标移到下一行; - 外层变量叫
i,内层叫j,这是惯例;再深一层的嵌套通常意味着该拆函数了。
用字符串乘法简化
def print_square(size):
for _ in range(size):
print("#" * size)
外层循环仍然需要,因为要打印多行;内层循环可以用字符串乘法替代。
抽象的价值
print_square 不需要知道「一行」是怎么打印的,它只需要一个 print_row:
def print_square(size):
for _ in range(size):
print_row(size)
def print_row(width):
print("#" * width)
这样 print_row 的实现可以随便换——用循环、用乘法、甚至以后换成打印图形——只要名字和参数不变,调用方完全不用改。这就是抽象:把「怎么做」藏起来,只暴露「做什么」。
本章结论
- 重复代码应当改成循环;循环把重复次数变成参数。
while需要手动初始化、判断、更新计数器;忘记更新就会无限循环,Ctrl-C可中断。i += 1是自增简写,Python 没有i++。for ... in range(n)自动生成 0 到 n-1;循环变量不用时命名为_。- 字符串可以用
*重复,配合\n与end=""控制排版。 while True+break是输入校验的标准结构;continue跳过本轮。- 列表用
[],索引从 0 开始;for x in list直接遍历元素,range(len(list))遍历索引。 - 字典用
{},按键查找;遍历字典拿到的是键。 None表示「没有值」,与空字符串含义不同。- 嵌套循环按「先行后列」输出;
print()单独调用只输出换行。 - 函数可以把复杂输出拆成可复用的抽象。
理解检验
1. 下面这段代码会发生什么?如何修正?
i = 3
while i != 0:
print("meow")
参考答案:会无限打印 meow。循环体里没有任何语句改变 i,条件 i != 0 永远为真。修正是在循环体里递减:
i = 3
while i != 0:
print("meow")
i -= 1
在终端中可以按 Ctrl-C 中断已经失控的循环。
2. range(3) 产生哪些值?for i in range(3) 循环几次?如果要循环到 3(含 3),应该怎么写?
参考答案:range(3) 产生 0, 1, 2,循环 3 次。它从 0 开始,到参数之前停止,不包含参数本身。要包含 3,写 range(4),产生 0, 1, 2, 3。
3. 为什么输入校验要用 while True 加 break,而不是写一次 if?
参考答案:一次 if 只能检查一次。用户如果连续输错,程序要么接受错误的值,要么直接结束,都无法继续追问。while True 让程序反复询问,直到条件满足才 break:
while True:
n = int(input("What's n? "))
if n > 0:
break
while True 是故意制造无限循环,break 是唯一的出口,因此出口条件必须写清楚。
4. 列表索引从 0 开始,写一个循环按「1. Hermione」这样的编号输出学生名单。
参考答案:
students = ["Hermione", "Harry", "Ron"]
for i in range(len(students)):
print(f"{i + 1}. {students[i]}")
输出:
1. Hermione
2. Harry
3. Ron
len(students) 给出长度 3,range(3) 给出 0、1、2,i + 1 把内部索引转换成人类习惯的编号。
5. for student in students 遍历一个字典时,student 拿到的是什么?怎样同时打印键和值?
参考答案:拿到的是键。要打印键和值,用键去索引字典:
students = {"Hermione": "Gryffindor", "Draco": "Slytherin"}
for student in students:
print(student, students[student], sep=", ")
输出:
Hermione, Gryffindor
Draco, Slytherin
6. 解释下面这段代码的每一层在做什么;如果去掉内层的 end="" 会发生什么?
def print_square(size):
for i in range(size):
for j in range(size):
print("#", end="")
print()
参考答案:外层循环 i 控制行数,循环 size 次;内层循环 j 控制每一行里的砖块数,也循环 size 次;print("#", end="") 打印一个 # 且不换行;内层结束后,单独的 print() 输出一个换行,把光标移到下一行。
如果去掉 end="",每个 # 都会换行,输出会变成一列竖排的 #,而不是一个 size × size 的方块。
7. 为什么 print_square 调用 print_row 比直接写字符串乘法更好?
参考答案:因为 print_square 只关心「打印 size 行」,不关心「一行怎么打印」。把后者交给 print_row,两层职责分开:以后要改一行的样式(比如用 ? 代替 #),只改 print_row,print_square 一行都不用动。这就是抽象的好处——调用方依赖的是名字和参数,不是实现细节。
与前后章的关系
循环把上一章的「按条件执行一次」扩展成「反复执行」,三者合起来构成完整的控制流。本章引入的列表与字典,是后面处理文件、数据与对象的基础结构;while True 的输入校验模式,会在异常处理一章被 try / except 取代或补充。
马里奥例子里的「先抽象、再实现」思路,会在函数、测试与面向对象章节不断复用。
What this chapter covers
This is chapter 3 of the long video, covering CS50P week 2 (02:46:38–04:07:34) and the subject of loops.
The starting point is a small defect: to make a cat meow three times, the simplest thing is three lines of print("meow"). It runs, but ask for fifty meows, five hundred, or a change from "meow" to "woof", and the code collapses. Loops exist to handle this entire class of problem: doing the same thing repeatedly.
The chapter follows that thread: a while loop with a hand-managed counter, including an accidental infinite loop; for with range, which hands the counting to Python; while True with break for input validation; lists and dictionaries, so loops can traverse data rather than numbers; and finally the Mario bricks, where loops, functions and abstraction come together.
Timeline
| Time | Topic |
|---|---|
| 2:46:38 | The week's theme: why loops exist |
| 2:47:00 | cat.py: the trouble with three print lines |
| 2:49:56 | The while loop and a counter |
| 2:52:06 | Infinite loops: why they are dangerous and how Ctrl-C saves you |
| 2:54:11 | Counting down so the program actually stops |
| 2:56:26 | Counting up from 1 to 3 |
| 2:58:15 | The convention of counting from 0, and i += 1 |
| 3:02:43 | for loops over the list [0, 1, 2] |
| 3:07:16 | range(3): stop hand-listing values for extreme cases |
| 3:09:04 | Using _ for a loop variable you do not care about |
| 3:09:35 | String multiplication: "meow\n" * 3 |
| 3:13:11 | Letting the user choose how many meows |
| 3:14:44 | while True with continue / break for validation |
| 3:18:05 | Packaging meow and get_number as functions |
| 3:21:06 | Lists: students = ["Hermione", "Harry", "Ron"] |
| 3:23:04 | Zero-based indexing; students[0] |
| 3:24:03 | for student in students to traverse a list |
| 3:27:26 | len() and range(len(students)) |
| 3:32:00 | Dictionaries: key-value pairs and curly braces |
| 3:38:15 | Iterating a dictionary yields its keys |
| 3:43:18 | A list of dictionaries: several attributes per student |
| 3:46:00 | None: the absence of a value |
| 3:51:28 | Mario: a column, a row, a square |
| 4:01:55 | Nested loops and print() for a newline |
| 4:05:03 | Simplifying the inner loop with string multiplication |
The problem: repeated code
print("meow")
print("meow")
print("meow")
Three identical lines. The problem is not "three lines" but what comes next:
- How do you meow three hundred times? Copy and paste three hundred lines?
- How do you change the cat to a dog? Edit three hundred places.
- How do you let the user decide the count? You cannot.
Loops turn repetition from how many lines you write into a parameter.
while: managing the counter yourself
while takes a boolean expression and repeats its indented block as long as that expression is true.
i = 3
while i != 0:
print("meow")
i = i - 1
This prints three lines. The order of execution:
i = 3;- test
i != 0, true; - print, then
ibecomes 2; - back to step 2, until
ireaches 0 and the test is false.
Infinite loops
Forget the update in step 3:
i = 3
while i != 0:
print("meow")
i stays 3 forever, the condition stays true, and the program prints without end. This accident is an infinite loop. In a terminal, press Ctrl-C to interrupt it; there is no need to reboot.
Note: this is the classic
whilebug. Whenever you write one, ask immediately: does the body contain the step that changes the condition?
Counting up, down and +=
Counting from 1 to 3:
i = 1
while i <= 3:
print("meow")
i = i + 1
Most programmers count from 0, because list indices start at 0:
i = 0
while i < 3:
print("meow")
i += 1
Note < 3 rather than <= 3: starting at 0 and stopping before 3 gives exactly three iterations.
i += 1 abbreviates i = i + 1. Python has no i++ — that is C, C++ and Java syntax.
for: letting Python count
for i in [0, 1, 2]:
print("meow")
for assigns each value in the list to i in turn and runs the body once per value. You neither initialise nor update the counter; i becomes 0, then 1, then 2.
But writing [0, 1, 2] by hand brings back the old problem: what about a million meows?
range
for i in range(3):
print("meow")
range(n) produces the integers from 0 up to but not including n. range(3) is 0, 1, 2.
A million meows is just range(1_000_000).
Naming an unused variable _
If the loop variable is never used, the Python convention is a single underscore:
for _ in range(3):
print("meow")
This tells the reader a counter is required but its value does not matter. Do not name a variable _ and then use it — that only confuses people.
String multiplication
Python lets * repeat a string:
print("meow\n" * 3, end="")
This prints three lines. "meow\n" is one unit including its newline; multiplying by 3 produces three units; end="" stops print from adding a fourth newline, which would leave a blank line.
The line is short, but less readable than a loop. The lecture's stance: it works, but do not sacrifice readability for cleverness.
Input validation: while True + break
Let the user choose the number of meows, while insisting on a positive number. A single if is not enough, since the user may get it wrong repeatedly. Use a loop:
while True:
n = int(input("What's n? "))
if n > 0:
break
while True is a deliberate infinite loop with break as the exit. The flow:
- the condition is always true, so enter the loop;
- read input;
- if
n > 0, break out; - otherwise do nothing and ask again.
To skip the rest of the current iteration but stay in the loop, use continue:
while True:
n = int(input("What's n? "))
if n < 0:
continue
break
Wrapping it in functions
def main():
number = get_number()
meow(number)
def get_number():
while True:
n = int(input("What's n? "))
if n > 0:
return n
def meow(n):
for _ in range(n):
print("meow")
main()
Two details:
get_numberusesreturn nto leave the function and hand back the value, which is cleaner than breaking and then returning;meowonly prints,get_numberonly obtains a number — separate responsibilities.
Lists: several values in one variable
students = ["Hermione", "Harry", "Ron"]
Square brackets denote a list. Access elements with brackets and an index, and indices start at 0:
print(students[0]) # Hermione
print(students[1]) # Harry
print(students[2]) # Ron
Traversal
for student in students:
print(student)
You do not need to know the length or use indices. Name the variable student rather than s or _, because it really is a student.
When you need indices
for i in range(len(students)):
print(i + 1, students[i])
len(students) returns the length, and range(len(...)) produces the indices. i + 1 is only for displaying a human-friendly count.
Dictionaries: key-value pairs
A list is indexed by number; a dictionary is indexed by a key you choose:
students = {
"Hermione": "Gryffindor",
"Harry": "Gryffindor",
"Ron": "Gryffindor",
"Draco": "Slytherin",
}
print(students["Hermione"]) # Gryffindor
Curly braces denote a dictionary, with key: value pairs. Look up a value by placing its key in brackets.
Iterating a dictionary:
for student in students:
print(student, students[student], sep=", ")
A for loop over a dictionary yields its keys, not its values. Output:
Hermione, Gryffindor
Harry, Gryffindor
Ron, Gryffindor
Draco, Slytherin
A list of dictionaries
When each student has several attributes:
students = [
{"name": "Hermione", "house": "Gryffindor", "patronus": "Otter"},
{"name": "Harry", "house": "Gryffindor", "patronus": "Stag"},
{"name": "Ron", "house": "Gryffindor", "patronus": "Jack Russell terrier"},
{"name": "Draco", "house": "Slytherin", "patronus": None},
]
for student in students:
print(student["name"], student["house"], student["patronus"], sep=", ")
None is Python's keyword for "no value", capitalised. Draco has no patronus, and None says that explicitly — an empty string "" could equally mean "I forgot to fill this in".
Mario: building with loops and abstraction
A column
def main():
print_column(3)
def print_column(height):
for _ in range(height):
print("#")
main()
A taller column means one changed argument.
A row
def print_row(width):
print("?" * width)
String multiplication reduces "print a row" to one line.
A square
def print_square(size):
for i in range(size):
for j in range(size):
print("#", end="")
print()
Output:
###
###
###
Key points:
- the outer loop handles rows, the inner loop handles bricks within a row;
- the inner
printusesend=""so bricks stay on the same line; - after the inner loop, a bare
print()emits one newline and moves to the next row; iandjare conventional names; a third level of nesting usually means it is time to extract a function.
Simplifying with string multiplication
def print_square(size):
for _ in range(size):
print("#" * size)
The outer loop is still needed to produce multiple rows; the inner loop can be replaced by multiplication.
The value of abstraction
print_square does not need to know how a row is printed:
def print_square(size):
for _ in range(size):
print_row(size)
def print_row(width):
print("#" * width)
print_row can now be reimplemented freely — with a loop, with multiplication, or later with graphics — and as long as its name and parameters stay the same, callers need not change. That is abstraction: hide the how, expose the what.
Chapter summary
- Repeated code should become a loop; a loop turns the repetition count into a parameter.
whilerequires you to initialise, test and update the counter; forget the update and you get an infinite loop, whichCtrl-Cinterrupts.i += 1abbreviates incrementing; Python has noi++.for ... in range(n)yields 0 through n-1; name an unused loop variable_.- Strings can be repeated with
*, and\nplusend=""controls layout. while Truewithbreakis the standard validation pattern;continueskips one iteration.- Lists use
[]and zero-based indices;for x in listtraverses elements,range(len(list))traverses indices. - Dictionaries use
{}and are looked up by key; iteration yields keys. Nonemeans "no value", which differs from an empty string.- Nested loops print row by row; a bare
print()emits only a newline. - Functions break complicated output into reusable abstractions.
Comprehension check
1. What happens in the following, and how should it be fixed?
i = 3
while i != 0:
print("meow")
Reference answer: it prints meow forever. Nothing in the loop body changes i, so i != 0 remains true. The fix is to decrement inside the loop:
i = 3
while i != 0:
print("meow")
i -= 1
In a terminal, Ctrl-C interrupts a loop that has already run away.
2. What values does range(3) produce? How many iterations does for i in range(3) run? How would you loop through 3 inclusive?
Reference answer: range(3) produces 0, 1, 2, so the loop runs 3 times. It starts at 0 and stops before the argument. To include 3, use range(4), which produces 0, 1, 2, 3.
3. Why use while True with break for input validation instead of a single if?
Reference answer: a single if checks once. If the user keeps entering bad values, the program either accepts a wrong value or exits, and cannot keep asking. while True keeps asking until the condition is met:
while True:
n = int(input("What's n? "))
if n > 0:
break
while True is an intentional infinite loop, and break is its only exit, so the exit condition deserves careful thought.
4. List indices start at 0. Write a loop that prints students numbered from 1, like "1. Hermione".
Reference answer:
students = ["Hermione", "Harry", "Ron"]
for i in range(len(students)):
print(f"{i + 1}. {students[i]}")
Output:
1. Hermione
2. Harry
3. Ron
len(students) gives 3, range(3) gives 0, 1, 2, and i + 1 converts the internal index into a human numbering.
5. When for student in students iterates over a dictionary, what does student hold? How do you print both key and value?
Reference answer: it holds the key. Index the dictionary by that key to print both:
students = {"Hermione": "Gryffindor", "Draco": "Slytherin"}
for student in students:
print(student, students[student], sep=", ")
Output:
Hermione, Gryffindor
Draco, Slytherin
6. Explain each layer of the following code. What happens if the inner end="" is removed?
def print_square(size):
for i in range(size):
for j in range(size):
print("#", end="")
print()
Reference answer: the outer loop i runs size times and controls rows; the inner loop j also runs size times and controls bricks per row; print("#", end="") prints one brick without a newline; after the inner loop, the bare print() emits a newline and moves to the next row.
Without end="", every # would end with a newline, producing a single vertical column instead of a size × size square.
7. Why is it better for print_square to call print_row rather than doing the string multiplication itself?
Reference answer: print_square only cares about printing size rows, not how one row is printed. Delegating the latter to print_row separates two responsibilities: if the row style changes later (say ? instead of #), only print_row changes and print_square stays untouched. That is the benefit of abstraction — callers depend on names and parameters, not on implementation details.
Relation to neighbouring chapters
Loops extend the previous chapter's "run once, conditionally" to "run repeatedly", and together the three constructs form a complete control flow. The lists and dictionaries introduced here underpin files, data processing and objects later in the course. The while True validation pattern will later be replaced or supplemented by try / except.
The "abstract first, implement second" habit from the Mario examples recurs in the chapters on functions, testing and object-oriented programming.