Reading and Writing Outside the Program
Everything so far has lived inside the program and vanished the moment it stopped running. Write a line to an actual file on disk, close it properly, and read the same line back in a program that starts fresh.
Everything so far has lived inside the program and vanished the moment it stopped running. Write a line to an actual file on disk, close it properly, and read the same line back in a program that starts fresh.
Opening a file is not the same as using it safely
$ >>> f = open("notes.txt", "w")$ >>> f.write("first line\n")$ 11$ >>> f.close()open("notes.txt", "w") creates the file if it does not exist yet, or empties it if it does — the "w" means write mode. write() returns the number of characters written, which is easy to ignore and easy to forget you are ignoring.
$ >>> f = open("notes.txt", "w")$ >>> f.write("first line\n")$ 11$ >>> 1 / 0$ Traceback (most recent call last):$ ZeroDivisionError: division by zero$ >>> f.closed$ FalseThe error skipped straight past f.close() — it was never reached. f.closed still reports False, and depending on the operating system's buffering, “first line” might not have actually reached the disk at all yet. That is the exact failure the next section fixes.
with closes the file even when something goes wrong
with wraps the open file in a block that closes it automatically the moment the block ends — including when an error is raised partway through, which a manual close() call would never reach.
$ >>> with open("notes.txt", "w") as f:$ ... f.write("first line\n")$ ... f.write("second line\n")$ ...$ >>> f.closed$ TrueReading uses the same shape, with "r" in place of "w" — and it is the default, so it can be left out entirely.
$ >>> with open("notes.txt") as f:$ ... contents = f.read()$ ...$ >>> print(contents)$ first line$ second lineCompare that to the same mistake as before, this time inside a with block.
$ >>> with open("notes.txt", "w") as f:$ ... f.write("first line\n")$ ... 1 / 0$ ...$ Traceback (most recent call last):$ ZeroDivisionError: division by zero$ >>> f.closed$ TrueThe exception still happens — with does not hide it, and code after the block still needs to handle it if you care. What changes is f.closed: True, because the file handle was released the instant the block exited, error or not. That is the entire justification for treating with as non-negotiable rather than a style preference: skipping it does not just look worse, it leaves a real resource open exactly when a crash makes it most likely nobody is coming back to close it by hand.
Text mode and binary mode are not the same open
Every open() so far has used the default, text mode — Python decodes whatever bytes are actually on disk into a str, using a text encoding, and encodes a str back into bytes on write. Add "b" to the mode string and that decoding step is skipped entirely: read() then hands back raw bytes, exactly as they sit on disk.
$ >>> with open("photo.png", "rb") as f:$ ... header = f.read(8)$ ...$ >>> header$ b'\x89PNG\r\n\x1a\n'Open an image, a zip file, or anything that is not text in plain "r" mode, and Python tries to decode bytes that were never meant to be text — usually raising UnicodeDecodeError partway through the file, on whichever byte sequence happens not to form valid text.
The decoding step in text mode has to pick a rule for turning bytes into characters, and that rule is the encoding. Python's default is usually UTF-8, but “usually” is exactly the problem: a file written on a different system, or by a different program, can be encoded differently, and opening it with the wrong encoding produces either an error or, worse, text that reads back subtly wrong without complaining at all.
with open("notes.txt", encoding="utf-8") as f: contents = f.read()Paths behave differently depending on where the program runs from
"notes.txt" is a relative path — Python looks for it relative to wherever the program was started from, not relative to the .py file itself. Run the same script from two different folders, and it can read two entirely different files, or fail to find one at all.
String concatenation
pathlib.Path
$ >>> from pathlib import Path$ >>> data_dir = Path("data")$ >>> notes_path = data_dir / "notes.txt"$ >>> notes_path$ PosixPath('data/notes.txt')$ >>> Path("notes.txt").resolve()$ PosixPath('/Users/you/project/notes.txt')$ >>> Path("notes.txt").exists()$ TrueReading a huge file line by line, instead of all at once
f.read() hands back the entire file as one string, which is fine for notes.txt and a real problem for a two-gigabyte log file — Python has to hold all two gigabytes in memory at once before your code even looks at the first line.
A file object is iterable, though, the same way a list is, and iterating over it yields one line at a time without ever holding the rest of the file in memory.
$ >>> with open("access.log") as f:$ ... for line in f:$ ... if "ERROR" in line:$ ... print(line.strip())$ ...At any point during that loop, exactly one line is in memory — the one currently being checked. Whether the file is nine lines long or nine million makes no difference to how much memory the loop uses, only to how long it takes to finish.
Key takeaways
- open("notes.txt", "w") creates or empties a file; "r", the default, reads it instead — and forgetting to close it can leave the write unfinished on disk.
- with closes the file the instant its block ends, even when an exception interrupts it partway through — that's not a style preference, it's the difference between a file handle actually being released and one that silently isn't.
- Text mode decodes bytes into a str using an encoding, usually UTF-8 by default; binary mode ("rb") skips decoding and hands back raw bytes — naming the encoding explicitly avoids a bug that only appears on someone else's machine.
- pathlib.Path builds a path with the operating system's own separator via /, rather than gluing strings together by hand, and the result is still a Path you can call .resolve() or .exists() on.
- Iterate over an open file with a for loop to process it one line at a time; f.read() and f.readlines() both load the entire file into memory first, which stops scaling once the file gets large.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.