File I/O and context managers

Opening a file, and the cleanup problem

open() gives you a file object — a handle to the actual file on disk, not the file’s contents directly:

Try it — edit and run

"r" is the mode:

  • "r" for reading (the default if omitted)
  • "w" for writing (creates the file if it doesn’t exist, and overwrites it completely if it does)
  • "a" for appending (writes get added to the end, the existing content is left alone)

f.close() releases the underlying operating system resource — skipping it isn’t just untidy, it can leave a file locked or its contents not fully flushed to disk.

The problem: f.close() only runs if execution actually reaches that line. If something raises an exception in between open() and close(), the file never gets closed — the same line-by-line execution model from the Python setup lesson means Python just stops wherever the error happened:

Try it — edit and run

with — guaranteed cleanup, even on exception

You’ve already seen the fix for “cleanup that must happen no matter what” — finally, from the error handling concept back in the Python setup lesson. File handling has dedicated syntax for exactly this pattern, since it’s common enough to deserve its own shorthand: with.

Try it — edit and run

The ZeroDivisionError still happens and still propagates — with doesn’t swallow exceptions — but the file is guaranteed to be closed before that traceback ever reaches you, whether the block finishes normally or crashes partway through. f is only valid inside the indented block; there’s no f.close() to remember or forget. This is Python’s version of Java’s try-with-resources — a dedicated syntax for “acquire something, guarantee its cleanup,” rather than relying on a programmer to remember a matching finally.

open() returning something usable with with is what makes it a context manager — an object that defines what should happen on entry (here, nothing extra — the file’s already open) and on exit (closing the file), regardless of how the block ends. You won’t write your own context manager in this course, but recognizing the pattern matters: any object usable after with ... as ...: is promising the same guarantee with open(...) gives you.

Reading a file: four ways, and when each fits

Try it — edit and run

.read() loads the entire file into memory as a single string, newlines and all — fine for small files, wasteful for anything large.

Try it — edit and run

.readline() reads exactly one line at a time, remembering where it left off between calls — useful when you need fine control over reading one line, checking it, and deciding whether to read further.

Try it — edit and run

.readlines() reads the whole file at once, same as .read(), but returns a list of lines instead of one string — still loads everything into memory up front.

Try it — edit and run

Directly iterating the file object with for line in f: is the most idiomatic option for processing a file line by line — a file object is itself an iterable, in exactly the sense covered back in the control flow concept, handing out one line at a time without ever loading the whole file into memory at once. For a genuinely large file, this matters: .read()/ .readlines() on a multi-gigabyte file can exhaust memory; the for version never holds more than one line at a time.

Since a file object is an iterator, next() works on it directly too, pulling one line at a time by hand — the same mechanism the for loop uses internally, just invoked one step at a time yourself:

Try it — edit and run

Writing to a file

Writing uses "w" or "a" mode and .write(), which — unlike print() — doesn’t add a newline automatically:

Try it — edit and run

Opening the same file again with "w" overwrites it completely — worth being careful about, since there’s no confirmation or undo:

Try it — edit and run

"a" mode instead adds to the end without touching what’s already there:

Try it — edit and run

.seek() and .tell() — moving around inside a file

A file object tracks its current read/write position internally — every .read(), .readline(), or next() call advances it. .tell() reports the current position (in bytes); .seek() jumps to a specific position directly:

Try it — edit and run

f.seek(0) is the common case — rewinding to re-read a file from the start within the same with block, without closing and reopening it.

Check your understanding
1/6

What does open("file.txt", "w") do if file.txt already exists and already has content?