[3, 2, 1]
Day 2: Control Flow, I/O, and Debugging
Studierendentage Physik, Uni Heidelberg, 23.-27.03.2026
list (ordered, mutable) — measurements over timetuple (ordered, immutable) — fixed coordinates (x, y, z)dict (key → value) — parameters/constantsset (unique elements) — unique labels/IDsImmutable means: you can’t change the elements inside the tuple (but you can assign a new tuple to a variable).
Lists are mutable and come with many useful methods:
.append, .extend, .insert, .pop, .remove.sort, .reverse+= (aliasing) vs .copy()= does not copy the list. It creates a second name for the same object.
Make an independent copy:
Slicing extracts parts of sequences: start:stop:step.
([1, 2, 3], [0, 1, 2], [0, 2, 4], [5, 4, 3, 2, 1, 0])
The “include start, exclude end” rule is used across Python. For example, range(5) gives 0, 1, 2, 3, 4 (not including 5).
This allows one to split sequences cleanly:
Works also for strings:
Trying to do pos[0] = 0.0 would raise an error.
Use a dict to keep named parameters together.
Rule of thumb: in Python, scope is mainly defined by functions, modules, and classes (not by if/for/while).
If you define s inside the function, it does not exist outside.
if / for / while blocks (no block scope)In Python, if/for/while blocks do not create a new scope (unlike C/C++/Java).
Common pitfall: the block might not run.
Write a program that prints the first 8 rows of Pascal’s triangle, nicely centered:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
Hint: each row can be computed from the previous one.
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
Why functions?
0.9
Rule: one function = one task. If a function does two things, split it.
A recursive function calls itself on a smaller version of the same problem.
Key idea:
n == 0)n - 1)Write a recursive function gcd(a, b) that computes the greatest common divisor of two positive integers.
Core idea of Euclid’s algorithm:
Any number that divides both a and b will also divide the remainder a mod b.
So we can repeatedly replace (a, b) by (b, a % b).
Requirements:
b == 0.gcd(84, 30).Optional challenge: handle negative inputs by returning a non-negative GCD.
You may see annotations like m: float or -> float.
You can provide a default value for a parameter. Then it becomes optional.
(19.62, 3.24)
Rule of thumb: defaults should be “safe” and make common cases easy.
When calling a function you can name arguments:
40.77471967380224
Keywords improve readability and allow reordering (but positional arguments must come first).
Aliases are common:
For scientific code you often see (requires NumPy):
Rule of thumb: put imports at the top of your file.
rePython provides the module re for working with regular expressions.
Task:
re is used.ab123: two lowercase letters followed by three digits.re.fullmatch(...) to test your pattern on a few examples.Example script file free_fall.py:
if __name__ == "__main__": means:
Run it in a terminal (in the folder where the file is):
Often you want “a new list from an old list”.
Pattern: [expression for item in iterable]
[0.0, 1.22625, 4.905, 11.03625, 19.62]
You can also filter with an if:
Random numbers are useful for noise/measurement uncertainty.
Read measurement data from a CSV-like text file (with comment lines).
file_path = "data/data_rate_vs_thickness_pb.csv"
thickness_mm = []
count_rate = []
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
t_str, r_str = line.split(",")
thickness_mm.append(float(t_str))
count_rate.append(float(r_str))
len(thickness_mm), thickness_mm[:3], count_rate[:3](9, [0.0, 3.0, 6.0], [52644.0, 43823.0, 35922.0])
with for files?Core pattern:
Why with is useful:
f.close() manually.Substract background rate (\(R_{net} = R_{meas} - 50\) counts/min).
background_rate = 50.0
net_count_rate = [r - background_rate for r in count_rate]
header = f"{'thickness_mm':>12} {'count_rate':>12} {'net_count_rate':>16}"
print(header)
print("-" * len(header))
for t, r, r_net in zip(thickness_mm, count_rate, net_count_rate):
print(f"{t:12.0f} {r:12.0f} {r_net:16.0f}")thickness_mm count_rate net_count_rate
------------------------------------------
0 52644 52594
3 43823 43773
6 35922 35872
9 29968 29918
12 24771 24721
15 20633 20583
18 16839 16789
21 13816 13766
24 11576 11526
Write the processed data to a new CSV file with a header row.
import csv
output_path = "output/data_rate_vs_thickness_pb_with_net.csv"
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["thickness_mm", "count_rate", "net_count_rate"])
for t, r, r_net in zip(thickness_mm, count_rate, net_count_rate):
writer.writerow([t, r, r_net])
print(f"Wrote {len(thickness_mm)} rows to {output_path}")Wrote 9 rows to output/data_rate_vs_thickness_pb_with_net.csv
When code fails, work in small steps:
This is usually faster than guessing.
A Python traceback tells you:
NameError, TypeError, …)Rule of thumb:
v = 4.0 m/s
assert is meant to detect bugs in your code, not handle normal runtime situations.
What it does:
time_s > 0)AssertionError with the given messageassert → “This should never happen unless there’s a bug”
if + raise → “This might happen, and we handle it”
The following script should compute background-corrected count rates from CSV-like lines.
def load_measurements(lines):
rows = []
for line in lines:
line = line.strip()
if not line or line.startswith("#"):
continue
t_str, r_str = line.split(",")
rows.append({"thickness_mm": float(t_str), "count_rate": r_str.strip()})
return rows
def net_count_rates(rows, background=50.0):
return [row["count_rate"] - background for row in rows]
lines = [
"# thickness_mm, count_rate",
"0, 1200",
"1, 980",
"2, 810",
]
rows = load_measurements(lines)
net = net_count_rates(rows)
print(net)Observed error message:
File "/Users/reygers/uni/Lehre/Vorlesungen/AI-assisted Python Coding SS2026/tmp/bug.py", line 22, in <module>
net = net_count_rates(rows)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/reygers/uni/Lehre/Vorlesungen/AI-assisted Python Coding SS2026/tmp/bug.py", line 12, in net_count_rates
return [row["count_rate"] - background for row in rows]
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~
TypeError: unsupported operand type(s) for -: 'str' and 'float'Tasks:
Keep track of how long you needed to find the bug without AI and how long the LLM took to find it.
pytestExample test file test_physics.py (assumes physics.py with kinetic_energy):
How to read this:
test_ is one test caseassert checks expected result vs computed resultpytestRun in terminal (in the project folder):
pytest uses test discovery: it searches recursively for files matching test_*.py or *_test.py and runs all functions starting with test_. The -q flag means quiet (compact output).
Typical result:
2 passed means both tests succeeded1 failed means one test result did not match expectationtry/exceptUse exceptions for expected runtime problems, for example:
Use the standard library module subprocess when a Python script should run an external program.
Basic pattern:
'Python 3.12.12'
Useful options:
capture_output=True: capture stdout and stderrtext=True: return strings instead of bytescheck=True: raise an exception if the command fails