All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
Everything so far arrived as a string somebody typed into the source.
Real data lives in files, and this is the last piece of Part 3: opening one
safely, reading it without loading a gigabyte into memory, and writing one back
without destroying what was already there. The lab runs a real filesystem, so
every file you write here genuinely exists until the page is reloaded.
Ready?
1
open(), and Why with Is Not Optional
open() gives you a file object. The second argument is the
mode, and it decides what you are allowed to do and what
happens to whatever was there before:
"r" read; the file must exist (the default)
"w" write; CREATES OR EMPTIES the file
"a" append; creates, and adds to the end
"x" create; fails if it already exists
An open file is a resource the operating system is holding for you, and
it has to be given back. A with block does that
automatically, at the end of the block, whether it finished normally or
raised:
with open("notes.txt", "w") as f:
f.write("first line\n")
# closed here, even if the write raised
Without it you must call f.close() yourself, and any
exception between the open and the close skips it. Data sitting in a
buffer that never got flushed is a file that looks empty for no visible
reason — which is why with is not a style preference.
write() does not add a newline
print() spoiled you. f.write("a") followed by
f.write("b") produces ab, on one line. Every
line you write needs its \n writing too.
Quick check
What does open("report.txt", "w") do to an existing report?
2
Three Ways to Read, and When Each Is Right
with open("log.txt") as f:
everything = f.read() # one string, whole file in memory
with open("log.txt") as f:
lines = f.readlines() # a list of lines, newlines still attached
with open("log.txt") as f:
for line in f: # one line at a time, nothing else held
...
The third is the one to reach for by default. It reads a line, hands it
over, and forgets it — so a four-gigabyte log costs the same memory as a
four-line one. The first two are fine when you know the file is small and
you need all of it at once.
Every line you read this way still has its newline on the
end. Comparing line == "END" against
"END\n" fails, which is why almost every loop over a file
starts with line = line.strip().
Reading a file that is not there raises
FileNotFoundError — which is worth catching only when a
missing file is a real possibility you have an answer for. A missing
config with a sensible default, yes. A missing input file the whole job
depends on, no: let it stop.
Quick check
Why iterate a file rather than call read()?
3
Writing Without Destroying Anything
The distinction that matters most in this whole module:
Empties it first
open(path, "w")
The truncation happens at the open, before a single write.
Keeps what is there
open(path, "a")
Every write goes on the end. This is what a log wants.
pathlib handles the common cases without any of this
ceremony, and handles paths properly on every operating system:
write_text and read_text are whole-file
operations, so the memory caveat applies — but for a config, a small
report or a JSON document they replace four lines with one, and
write_text uses mode "w", so the same
truncation rule applies.
One more thing worth knowing before it bites: text files have an
encoding. The default depends on the machine, which
means a file that reads fine on your laptop can raise
UnicodeDecodeError on a server. Passing
encoding="utf-8" explicitly removes an entire class of
deployment surprise.
Quick check
A daily job opens its report with mode "w", then crashes before writing. What is in the file?
4
Read, Transform, Write
Almost every file job is the same three steps, and keeping them separate
is what makes the middle one testable:
def clean(lines): # no files in here at all
return [l.strip() for l in lines if l.strip()]
raw = Path("in.txt").read_text().splitlines()
Path("out.txt").write_text("\n".join(clean(raw)) + "\n")
clean takes a list and returns a list. It can be tested in
one line, with no file anywhere near it — which is exactly the argument
from module 3-01, applied to the messiest part of real work.
A function that opens a file, transforms its contents and writes another
cannot be tested without a filesystem, and every test of it is slower and
more fragile than it needed to be.
1
Read at the edge
One place that knows about paths.
2
Transform in the middle
Lists and dictionaries in, lists and dictionaries out.
3
Write at the other edge
One place that knows the output format.
4
Never in the middle
A transform that also writes is two jobs, and neither can be checked on its own.
This lab has a real filesystem
Files you write here genuinely exist, in memory, inside the tab. They
survive between runs of the same exercise — so an exercise that appends
will keep appending if you press Run twice, and one that starts by
writing its input fresh will not. Reloading the page wipes everything.
Nothing you do here can reach a file on your computer. The sandbox has
no route to it.
Quick check
Why split reading, transforming and writing into separate functions?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
open() takes a path and a mode. "w" writes,
"r" reads, and a with block closes the file at
the end whether or not anything went wrong.
with open("notes.txt", "w") as f: f.write("first\n")
write() adds no newline of its own —
print() spoiled you. Every line you write needs its
\n writing too.
Your task: write three lines to
notes.txt, then read the whole file back and print it, then
print its length in characters:
alpha beta gamma 17
your_code.py
PythonCtrl↵ to run
Hint
Two with blocks: one opening in "w" to write "alpha\n", "beta\n" and "gamma\n", one opening in "r" to read. contents = f.read() in the second.
Output
02
To do
This code opens a file, raises before reaching close(), and
leaves the file open with its data still sitting in a buffer. The file
exists and is empty, for no reason visible in the file itself.
A with block closes the file when the block ends — normally
or by exception. That is the whole reason it exists, and why calling
close() by hand is a thing you should almost never do.
Your task: rewrite it with a with block, so
that even though the calculation still fails, the two lines already
written are safely on disk:
could not finish: division by zero ['alpha', 'beta']
your_code.py
PythonCtrl↵ to run
Hint
Put the open in a with block and indent the writes and the try inside it. Then the close line goes away entirely — the block does it.
Output
03
To do
Looping over a file hands you one line per pass and holds nothing else, so
a four-gigabyte log costs the same memory as a four-line one.
Every line arrives with its newline still attached, which is why almost
every loop over a file starts by stripping it. Comparing
line == "END" against "END\n" fails, silently
and confusingly.
Your task: read queue.txt line by line,
skipping blanks and comments, and print each remaining job numbered:
1. build 2. test 3. deploy Jobs: 3
your_code.py
PythonCtrl↵ to run
Hint
with open("queue.txt") as f: then for line in f: — strip each line, continue past the empty ones and the ones starting with #, and append the rest.
Output
04
To do
This is the most expensive mistake in the module. "w" empties
the file at the moment it is opened, before anything is
written. Run a job twice with "w" inside the loop and only
the last entry survives.
"w" empties, then writes "a" keeps everything, adds to the end
A log wants "a". A report generated fresh each time wants
"w". Choosing is a decision, not a default.
Your task: run it and watch the log come out with one
line in it. Then fix the mode so all three entries survive:
['boot', 'retry', 'done'] 3
your_code.py
PythonCtrl↵ to run
Hint
The mode inside the loop is the problem. Leave the first block alone — that one is deliberately starting the log empty — and change the one in the loop to "a".
Output
05
To do
Most file reading is not to print the file — it is to work something out
from it. The reading is the boring part; the counting is the job.
Your task: read the log and report on it: how many
lines, how many are errors, and the longest line's length. Skip blank
lines entirely.
Lines: 4 Errors: 2 Longest: 18
your_code.py
PythonCtrl↵ to run
Hint
Loop the file, strip each line, continue past the blanks. Then add one to total, add one to errors when the line starts with "ERROR", and keep the longest with an if comparing len(line) to longest.
Output
06
To do
pathlib replaces four lines with one for the common cases,
and joins paths correctly on every operating system:
write_text uses mode "w", so the same
truncation rule applies — it is shorter, not safer.
Your task: build the path data/notes.txt,
make its directory, write two lines, then print the lines, whether it
exists, its suffix, and its name:
['one', 'two'] True .txt notes.txt
your_code.py
PythonCtrl↵ to run
Hint
Path("data") / "notes.txt" builds the path. p.parent.mkdir(parents=True, exist_ok=True) makes the folder without complaining if it is already there. Then write_text, and read_text().splitlines() for the first line of output.
Output
07
To do
Opening a file that does not exist raises
FileNotFoundError. Catch it only when a missing file is a
real possibility you have an answer for.
A missing config with sensible defaults, yes. A missing input file the
whole job depends on, no — let it stop, loudly, with the path in the
message.
Your task: write load_config(path), which
returns the file's lines, or an empty list if the file is not there.
Print the result for a file that exists and one that does not:
['port=8080', 'debug=false'] [] 2 0
your_code.py
PythonCtrl↵ to run
Hint
try: open the path and return f.read().splitlines(). except FileNotFoundError: return []. An empty list is a real answer here — the caller can loop over it without checking.
Output
08
To do
A comma-separated file is the most common thing you will be handed. Read
it, split each line, convert what needs converting, and it becomes the
lists and dictionaries from Part 2.
The first line is usually a header, and it is not data — skipping it is
the step everybody forgets exactly once.
Your task: read stock.csv, skip the header,
and build records as a list of dictionaries. Then print the
count, one record, and the total value:
Read the lines, then walk them from index 1 onwards to skip the header — or use enumerate and continue on the first. Split each row into three, convert the two numbers, and append a dictionary with the keys name, quantity and price.
Output
09
To do
Almost every file job is read, transform, write. Keeping the middle step
in a function that takes a list and returns a list is what makes it
testable — no files anywhere near it.
def clean(lines): return [l.strip() for l in lines if l.strip()]
A function that opens a file, transforms it and writes another cannot be
checked without a filesystem, and every test of it is slower and more
fragile than it needed to be.
Your task: write clean(lines) — which takes
a list and returns a list, dropping blanks and comments and upper-casing
the rest — then use it to turn in.txt into
out.txt:
['BUILD', 'TEST', 'DEPLOY'] 3
clean must not open, read or write anything.
your_code.py
PythonCtrl↵ to run
Hint
clean is one comprehension: keep the stripped line when it is non-empty and does not start with #, and upper-case it. Then read in.txt into a list, pass it through clean, and write "\n".join(...) + "\n" to out.txt.
Output
The log rotation
To do
Every night a service's log is rotated: the day's entries are cleaned into
an archive file, a summary is written alongside it as JSON, and the rest of
the pipeline reads those two rather than the raw log. Write that job.
The raw log has blank lines, comment lines from the rotation tool, and
trailing whitespace. A real entry is
timestamp LEVEL message, padded so the levels line up.
Write these two, with exactly these names:
parse(lines) — takes a list of raw lines and returns a list
of (level, message) tuples, dropping blanks and comments.
It must not open, read or write anything.
summarise(entries) — takes that list and returns a dictionary
with total, levels (a plain dict of counts) and
first_error (the message of the first ERROR, or
None). Also no files.
Then run the job: read service.log, write every
cleaned entry to archive.log as LEVEL message, one
per line, and write the summary to summary.json with sorted
keys. Then read both files back and report.
Print exactly five lines:
Archived: 5 entries INFO: 2 WARN: 1 ERROR: 2 First error: db timeout after 30s
The level lines are in the order INFO, WARN, ERROR — not alphabetical, and not whatever order they happened to appear in.
your_code.py
PythonCtrl↵ to run
Hint
parse: strip each line, skip the empty ones and the ones starting with #, split with maxsplit=2 into stamp, level and message, and collect (level, message) — remembering to strip the message, since the log is padded. summarise: total is len(entries); levels is a counting loop or a Counter converted with dict(); first_error is the message of the first entry whose level is ERROR, and None when there is none. Then write the archive with "\n".join(f"{level} {message}" for ...) and json.dump the summary. For the level lines, loop over ["INFO", "WARN", "ERROR"] rather than over the dictionary.