Files and Context Managers

This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Reading and writing files is one of the main places Python asks you to release a resource once you’re done with it — the with statement and the context manager protocol behind it exist largely for this reason.

Opening, Reading, and Writing Files

open() returns a file object for a given path and mode: "r" (read text, the default), "w" (write, truncating), "a" (append), and "b" combined with any of those for binary I/O ("rb", "wb"). The full mode table and encoding rules are in the tutorial’s Reading and Writing Files section (see also the open() reference for every argument).

f = open("notes.txt", "w")
f.write("first line\n")
f.write("second line\n")
f.close()                       # must close explicitly to flush and release the handle

Reading offers .read() (the whole file, or up to a given size, as one string), .readline() (one line at a time), and plain iteration (line by line, memory-efficient for large files):

f = open("notes.txt", "r")
whole = f.read()                # entire remaining contents as one string
f.close()

f = open("notes.txt", "r")
first = f.readline()            # 'first line\n'
f.close()

f = open("notes.txt", "r")
for line in f:                  # iterate lazily, one line per iteration
    print(line.rstrip("\n"))
f.close()

Every one of these examples has a bug waiting to happen: if .write() or .read() raises, .close() is never reached, and the underlying file descriptor leaks.

The with Statement

The idiomatic fix is not a manual try/finally — it’s the with statement, specified in the language reference’s with statement section. open() returns a context manager, so .close() is guaranteed to run when the block exits, exception or not:

# Equivalent to the with-statement below, spelled out manually
f = open("notes.txt", "r")
try:
    data = f.read()
finally:
    f.close()                   # always runs, even if .read() raised

# Idiomatic: with guarantees the close()
with open("notes.txt", "r") as f:
    data = f.read()
# f.close() has already happened here, on every exit path

A single with can open several context managers at once, each closed in reverse order when the block ends:

with open("in.txt") as src, open("out.txt", "w") as dst:
    for line in src:
        dst.write(line.upper())

pathlib.Path: the Modern Alternative to os.path

pathlib.Path represents a filesystem path as an object instead of a plain string, so joining paths uses the / operator and checks/reads are methods rather than free functions from os.path. Full reference: the pathlib module documentation.

from pathlib import Path

base = Path("data")
config_path = base / "config" / "settings.txt"   # join with '/', not os.path.join(...)

print(config_path.exists())                       # True/False, no os.path.exists() call needed

For text files, Path.read_text() opens, reads, and closes in a single call — no with block needed for this common case, since the file handle never outlives the method call:

from pathlib import Path

contents = Path("notes.txt").read_text()          # open + read + close, in one expression
Path("notes.txt").write_text(contents.upper())     # open + write + close

See Path.read_text() and Path.write_text() for their encoding and error-handling arguments. pathlib has far more to it than path joining and text I/O — globbing, renaming, stat info, and more — covered in full in Standard Library Tour's pathlib section rather than repeated here.

The Context Manager Protocol

with EXPR as x: is not special-cased to files — it works with any object implementing two methods, enter() and exit(), together called the context manager protocol (same language-reference section as above). enter() runs first and its return value is bound to x; the block body then runs; exit() always runs when the block ends, whether it ended normally or via an exception. If an exception occurred, exit() receives its type, value, and traceback as three arguments — and if exit() returns a truthy value (typically True), that exception is treated as handled and does not propagate past the with block.

A with Ctx() as x block: __enter__() is called first and its return value bound to x, then the block body runs, then __exit__() is called on exit — with a branch showing that __exit__() returning True swallows an in-flight exception while returning False lets it propagate
with open("notes.txt") as f:      # f.__enter__() returns f itself; bound to 'f'
    data = f.read()
    raise ValueError("oops")      # f.__exit__(ValueError, ..., ...) still runs, then re-raises

Writing a custom class with its own enter/exit — including one whose exit swallows a particular exception by returning True — is covered on Operator Overloading, which this page defers to rather than duplicating.

See Also

  • Exceptions — the try/except/finally machinery that with replaces for resource cleanup, and how exceptions interact with exit().

  • Operator Overloading — implementing a custom context manager’s enter and exit methods.