Paths and Text Files
Treat file locations, text encodings, and resource lifetimes as deliberate parts of your program.
By the end, you’ll be able to…
- Build portable paths with pathlib.
- Choose safe text-reading and writing strategies.
- Distinguish missing files, empty content, and decoding failures.
01A path describes a location
A filename such as notes.txt is relative to the process working directory. That directory might be your project while developing, a scheduler directory tomorrow, or a temporary folder in a test. A script does not automatically resolve relative paths beside its source file. Accept a base directory from the caller, or deliberately derive one, instead of assuming how the program was launched.
Use pathlib.Path to join components with the / operator. This expresses a filesystem operation without hard-coding a slash convention. Path objects are descriptions: constructing one neither creates the directory nor proves that a file exists. Separate choosing the location from opening it so both decisions remain visible.
02Text is decoded bytes
A file stores bytes. Text mode decodes those bytes into strings and encodes strings when writing. Specify UTF-8 when that is your data contract; relying on a machine-specific default can turn a working import into a deployment failure. An encoding error is evidence that the bytes and chosen contract disagree. Silently ignoring undecodable bytes can corrupt names, identifiers, or quantities.
read_text is convenient for a small configuration or report. For a large export, iterate over an opened file so the whole document need not fit in memory. The with statement closes the handle even if processing a line raises an exception. Strip only what the task requires: rstrip("\n") removes newline characters, whereas strip() also discards meaningful surrounding whitespace.
03Write with an explicit replacement policy
Writing with write_text or open(..., "w") replaces existing content. Appending with "a" preserves previous content but can repeat records if a job is retried. Choose the behavior from the application requirement before selecting the mode. For a new-only output, mode "x" refuses to overwrite an existing file.
A check such as if not path.exists() followed by opening does not guarantee that another process cannot create the file between those operations. Opening in the desired mode lets the filesystem enforce the relevant condition. Similarly, catch FileNotFoundError around the operation when a missing input has a defined recovery path.
04Make file behavior testable
Pass Path values into a function rather than burying a global filename in its body. A test can then supply a temporary directory, inspect the result, and let cleanup happen automatically. Return a useful value, such as the number of records written, so callers can verify behavior without scraping printed messages.
An empty file is valid content in some workflows; a missing file is an access failure; malformed text is a format failure. Keep these cases distinct. If you catch all three and return an empty list, downstream code can report a successful zero-record import when the input was never read. Good file handling preserves enough information to make that difference observable.
The ideas worth keeping.
- Resolve paths deliberately instead of depending on launch location.
- Specify text encodings and close resources with with.
- Choose overwrite, append, or exclusive creation from the data policy.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Count meaningful lines
Write count_nonblank(path) that reads UTF-8 text one line at a time and counts lines containing something other than whitespace. Demonstrate it safely.
Explore a worked solution
from pathlib import Pathfrom tempfile import TemporaryDirectorydef count_nonblank(path): with path.open(encoding="utf-8") as handle: return sum(1 for line in handle if line.strip())with TemporaryDirectory() as folder: path = Path(folder) / "notes.txt" path.write_text("one\n \ntwo\n", encoding="utf-8") assert count_nonblank(path) == 2The generator consumes the file incrementally. strip() is appropriate here because whitespace-only lines are intentionally excluded.
Protect an existing export
Create a UTF-8 report only if its destination is unused. Verify that a second attempt raises FileExistsError and leaves the first content intact.
Explore a worked solution
from pathlib import Pathfrom tempfile import TemporaryDirectorywith TemporaryDirectory() as folder: path = Path(folder) / "report.txt" with path.open("x", encoding="utf-8") as handle: handle.write("first\n") try: with path.open("x", encoding="utf-8") as handle: handle.write("second\n") except FileExistsError: pass assert path.read_text(encoding="utf-8") == "first\n"Exclusive creation enforces the policy during opening. Checking existence first would introduce a gap between the check and the write.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Count meaningful lines
Write count_nonblank(path) that reads UTF-8 text one line at a time and counts lines containing something other than whitespace. Demonstrate it safely.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.