Today's outcomes
- Choose read, write, append, or exclusive creation.
- Use with and pathlib for local files.
- Preserve line boundaries and distinguish text from bytes.
Before you start: Complete Day 15 and its practical challenge.
Understand the concept
A file extends a program's data beyond one run
Variables disappear when a process ends unless data is saved elsewhere. A local file can preserve a report or input dataset for the next run. Reading and writing introduce new boundaries: file location, encoding, permissions, existing contents, and the representation used on disk.
Use pathlib to represent a filesystem path, and use a context manager when working with an open file. with arranges for the file to be closed when its block exits, including when an exception interrupts ordinary execution. That resource-management guarantee does not mean a partially written report is automatically rolled back.
Every example in this lesson uses a temporary directory that is removed afterward. This makes experiments repeatable and avoids accidentally replacing a personal file. For your own saved work, choose a dedicated practice directory and inspect the destination before using a destructive write mode.
File modes encode an intent
Read mode r opens an existing file for reading. Write mode w creates a file or truncates an existing one. Append mode a adds content at the end. Exclusive creation mode x refuses to replace an existing file. Add b for binary data; ordinary text mode decodes or encodes strings.
Appending is not the same as editing an existing record. Re-running an append script can duplicate the same report line. Re-running a write script can replace earlier content. Choose the operation from the persistence contract and test two runs, not just the initial creation.
Encoding and line boundaries need explicit rules
Use an explicit encoding such as UTF-8 for text you control. Encoding translates strings to bytes and back; it is not the same as Unicode normalization from text processing. A wrong encoding can fail or produce wrong characters before any cleaning function sees them.
Newline characters separate records in a simple line-oriented file. write does not automatically append a newline. read returns text including line boundaries. splitlines creates logical lines without their terminators by default. If blank lines are meaningful, do not delete them without a stated rule.
Reading an entire small file is clear for learning. A very large file may need line iteration or chunking. “This example works” does not imply loading every file into memory is the appropriate production strategy.
Worked example
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "study.txt"
with path.open("w", encoding="utf-8") as handle:
handle.write("lists: 30\n")
handle.write("functions: 60\n")
with path.open("a", encoding="utf-8") as handle:
handle.write("strings: 45\n")
with path.open("r", encoding="utf-8") as handle:
text = handle.read()
lines = text.splitlines()
assert lines == ["lists: 30", "functions: 60", "strings: 45"]
assert text.endswith("\n")
print("Logical lines:", len(lines))
print(text, end="")
| Operation | Effect on disk | Evidence |
|---|---|---|
| Open w | Fresh report content | Earlier contents would be replaced |
| Two write calls | Two newline-terminated records | Each call supplies its newline |
| Open a and write | One additional record | Earlier two records remain |
| Read and splitlines | Three logical lines | Round-trip assertion |
The report survives closing and reopening the file within the temporary directory. It does not survive the directory's final cleanup, which is intentional for this exercise.
Guided practice
- Predict: write two values without newline characters. What text will read return? Explain why two write calls do not inherently mean two lines.
- Modify: run the append step twice and inspect the duplicate final record. Decide whether your own log should permit duplicates or needs a stable event identity.
- Reconstruct: write one non-ASCII topic label such as café, read it back with UTF-8, and compare the exact string. Keep a test that includes the newline policy too.
A relative path is interpreted from the current working directory, which may differ between a terminal and an editor. If a file “disappears,” inspect where the program actually wrote it before assuming the write failed.
Daily challenge
Implement write_topics(path, topics) and read_topics(path). Topics are strings without embedded line breaks. Write one topic per line using UTF-8 and replacement semantics: the new list becomes the complete file content. Read the same list back. An empty list should produce an empty file and read back as an empty list.
| Topics | Round-trip result |
|---|---|
['lists', 'functions'] |
Same two labels in order |
['café'] |
Exact Unicode label preserved |
[] |
Empty list |
| Write two topics, then write one | Only the new single topic remains |
Use a temporary directory for all acceptance cases. The contract does not permit newline characters inside a label because they would be confused with record separators. More general records need a structured format such as JSON on Day 25.
Reference solution
Compare after attempting the challenge
from pathlib import Path
from tempfile import TemporaryDirectory
def write_topics(path, topics):
text = "".join(topic + "\n" for topic in topics)
Path(path).write_text(text, encoding="utf-8")
def read_topics(path):
return Path(path).read_text(encoding="utf-8").splitlines()
with TemporaryDirectory() as directory:
path = Path(directory) / "topics.txt"
for topics in [["lists", "functions"], ["café"], []]:
write_topics(path, topics)
assert read_topics(path) == topics
write_topics(path, ["lists", "functions"])
write_topics(path, ["strings"])
assert read_topics(path) == ["strings"]
print("UTF-8 round trips passed")
The generator expression inside join supplies text pieces without needing a temporary list. Its role here is simple: one newline-terminated string per topic. pathlib's convenience methods handle opening and closing for these small whole-file operations.
Debug and explain
If old data disappears, inspect w mode. If records repeat, inspect a mode and repeated executions. If a label changes after saving, inspect encoding and newline assumptions. If a read raises FileNotFoundError, distinguish an absent file from an empty existing file.
Explain which operation in this challenge intentionally replaces earlier data, why the Unicode round-trip matters, and why with does not guarantee transaction rollback. Tomorrow you will handle known file and conversion failures without silently turning them into valid-looking empty results.
Review and next step
Keep the four round-trip cases and a note about current working directory. Rebuild the persistence contract before copying the solution. Day 17 adds explicit validation and targeted exceptions around these boundaries.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.