Warm-up · Activity 1 of 7
// I1.3 · ~30 min · Intermediate
Clean-up with finally and with
After this lesson you can make clean-up code run however a block ends, predict what return and break do next to finally, and let with close your files.
You will be able to
- Predict the order in which try, except, else and finally run, with and without an error
- Predict what return, break and an unhandled exception do when there is a finally clause
- Close files with with, also when the block fails, and open two files in one with
Predict · Activity 2 of 7
Predict before you read on: what does this print?
log = [] def divide(a, b): try: log.append("try") result = a / b except ZeroDivisionError: log.append("except") else: log.append("else") finally: log.append("finally") divide(1, 0) print(log)Practice · Activity 3 of 7
Fill in the keyword so that "cleanup" is appended even though the try block raises, and the ValueError still reaches the caller.
def use(log): try: log.append("work") raise ValueError("boom") ____: log.append("cleanup"):Practice · Activity 4 of 7
return inside try. What does this print?
def save(): try: print("saving", end=" ") return "saved" finally: print("closing", end=" ") print(save())Practice · Activity 5 of 7
A function has a try statement with except ValueError and finally. Match each case to what happens.
Brain teaser · Activity 6 of 7
Brain teaser. Python 3.14 prints a SyntaxWarning for this code, but it runs. What does it print?
def risky(): try: return 1 / 0 finally: return "finally wins" print(risky())Apply · Activity 7 of 7
Mini-task. Write copy_numbered(src, dst). It copies a text file, putting the line number, a colon and a space in front of every line, and returns the number of lines. Open both files in a single with statement, with encoding="utf-8". Try it on a file with two lines.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Counting lines, whatever goes wrong
count_lines opens a file with with and raises ValueError for an empty file. Its finally clause reports that it has finished, for a good file, an empty one and one that does not exist. Every file object it opened is kept in a list, so the last line can check that all of them were closed, even the one that raised.
main.py
from typing import TextIO
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("buy milk\ncall Ada\n")
with open("empty.txt", "w", encoding="utf-8") as f:
pass
opened: list[TextIO] = []
def count_lines(path: str) -> int:
try:
with open(path, encoding="utf-8") as f:
opened.append(f)
lines = f.readlines()
if not lines:
raise ValueError(f"{path} is empty")
return len(lines)
finally:
print(f"finished {path}")
for path in ["notes.txt", "empty.txt", "missing.txt"]:
try:
print(path, "has", count_lines(path), "lines")
except ValueError as err:
print("error:", err)
except OSError as err:
print("cannot open:", err.filename)
print("all closed:", all(f.closed for f in opened))
Run it with
python main.pyOutput
finished notes.txt
notes.txt has 2 lines
finished empty.txt
error: empty.txt is empty
finished missing.txt
cannot open: missing.txt
all closed: True- finished comes before has 2 lines: count_lines, with its finally, runs before print can show its result.
- finally runs even for missing.txt, where open fails before the with block starts.
- The file that raised ValueError is closed too: with closes it on the way out.
- Only two files were ever opened; the missing one never became a file object.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 2
Clean-up that always runs
run_step(name, log, fail) appends "start <name>", raises RuntimeError("<name> failed") when fail is true, and otherwise appends "done <name>". Make it always append "end <name>" as its last entry, also when it raises, and let the RuntimeError reach the caller.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Put the start, the check and the done line in a try block.
Hint 2
The end line goes into finally, so it runs whether or not the try block raised.
Hint 3
Do not add an except clause: the RuntimeError has to go on to the caller after finally.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
def run_step(name: str, log: list[str], fail: bool) -> None:
try:
log.append(f"start {name}")
if fail:
raise RuntimeError(f"{name} failed")
log.append(f"done {name}")
finally:
log.append(f"end {name}")
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
def run_step(name: str, log: list[str], fail: bool) -> None:
log.append(f"start {name}")
if fail:
raise RuntimeError(f"{name} failed")
log.append(f"done {name}")
log.append(f"end {name}")
test_main.py
from main import run_step
def test_success():
"""A step that works logs start, done and end"""
log = []
run_step("a", log, False)
assert log == ["start a", "done a", "end a"], f"the log is {log!r}"
def test_failure_logs_end():
"""A failing step still logs end"""
log = []
try:
run_step("b", log, True)
except RuntimeError:
pass
assert log == ["start b", "end b"], f"the log is {log!r}"
def test_failure_reaches_caller():
"""The RuntimeError reaches the caller"""
try:
run_step("c", [], True)
except RuntimeError as err:
assert str(err) == "c failed", f"the message was {str(err)!r}"
else:
assert False, "run_step returned normally; the RuntimeError must reach the caller"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
Two files, one with
copy_upper(src, dst) copies a text file in upper case and returns the number of lines. It works, but it opens dst first, so a missing src leaves an empty dst behind. Rewrite it with a single with statement that opens src first, both with encoding="utf-8", so a missing src raises FileNotFoundError before dst exists.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
One with statement can open two files, separated by a comma: with open(…) as fin, open(…) as fout:.
Hint 2
Files open from left to right. Put src first: if it is missing, the second open never happens.
Hint 3
Inside the block, loop over fin, write line.upper() to fout and count. No close() calls are needed.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
def copy_upper(src: str, dst: str) -> int:
count = 0
with open(src, encoding="utf-8") as fin, open(dst, "w", encoding="utf-8") as fout:
for line in fin:
fout.write(line.upper())
count += 1
return count
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
def copy_upper(src: str, dst: str) -> int:
count = 0
with open(dst, "w", encoding="utf-8") as fout:
with open(src, encoding="utf-8") as fin:
for line in fin:
fout.write(line.upper())
count += 1
return count
test_main.py
import os
from main import copy_upper
def test_copy():
"""The copy is in upper case, line by line"""
with open("in.txt", "w", encoding="utf-8") as f:
f.write("hello\nworld\n")
count = copy_upper("in.txt", "out.txt")
with open("out.txt", encoding="utf-8") as f:
got = f.read()
assert got == "HELLO\nWORLD\n", f"out.txt holds {got!r}"
assert count == 2, f"copy_upper returned {count!r}, expected 2"
def test_missing_source():
"""A missing source raises FileNotFoundError and creates no target"""
if os.path.exists("never.txt"):
os.remove("never.txt")
try:
copy_upper("no_such_file.txt", "never.txt")
except FileNotFoundError:
pass
else:
assert False, "copy_upper returned normally for a missing source"
assert not os.path.exists("never.txt"), "never.txt was created although the source is missing; open src first"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Writing after the with block
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("first line\n")
f.write("second line\n")
What Python prints
ValueError: I/O operation on closed file.Why, and the fix
with closes the file when its block ends. The name f still exists afterwards, but the file behind it is closed. Indent every line that uses the file into the with block, or open the file again.
Closing in finally what was never opened
try:
f = open("missing.txt", encoding="utf-8")
print(f.read())
finally:
f.close()
What Python prints
NameError: name 'f' is not definedWhy, and the fix
open failed, so f was never assigned, and finally then fails too, on top of the FileNotFoundError. Open the file before the try (then finally only runs once it is open), or simply use with open(...) as f:, which closes only what it opened.
else after finally
try:
x = 1
finally:
print("done")
else:
print("ok")
What Python prints
SyntaxError: invalid syntaxWhy, and the fix
The clauses have a fixed order: try, except, else, finally. else also needs at least one except clause before it. Move finally to the end and add the except clause that else depends on, or drop else.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.