Skip to content
elephantoo

Files & context managers

Lesson 17 of 38 15 min read

Read and write text and binary files, file modes, pathlib basics, the with statement and custom context managers.


Variables vanish when your program ends. To keep data — logs, settings, reports, exports — you write it to files. Python makes reading and writing files simple, and the with statement makes doing it safely just as simple. Along the way you'll learn what a context manager is and how to write your own.

Writing a text file#

Python
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("First line\n")
    f.write("Second line\n")
    print("Third line", file=f)      # print can write to files too

print("done")
Output
done

open(path, mode, encoding=...) returns a file object. Note:

  • write() does not add a newline — include \n yourself (or use print(..., file=f)).
  • Always pass encoding="utf-8" for text files. Without it Python uses the platform default, which differs between Linux and Windows and causes mojibake bugs with non-English text.
  • When the with block ends, the file is closed automatically and buffered data is flushed to disk.

Reading a text file#

Assume notes.txt from above exists. There are several ways to read it:

Python
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("First line\nSecond line\nThird line\n")

with open("notes.txt", encoding="utf-8") as f:     # mode defaults to "r"
    content = f.read()                              # whole file as one string
print(repr(content))

with open("notes.txt", encoding="utf-8") as f:
    for line in f:                                  # one line at a time
        print(line.rstrip("\n"))

with open("notes.txt", encoding="utf-8") as f:
    lines = f.read().splitlines()                   # list without newlines
print(lines)
Output
'First line\nSecond line\nThird line\n'
First line
Second line
Third line
['First line', 'Second line', 'Third line']

Iterating over the file is the idiomatic way to process large files: Python reads one line at a time, so even multi-gigabyte logs use almost no memory. f.read() and f.readlines() load everything at once — fine for small files only.

Each line from iteration keeps its trailing \n, which is why we call rstrip("\n") (or strip()).

File modes#

ModeMeaning
"r"read (default); error if missing
"w"write; erases existing content, creates if missing
"a"append to the end; creates if missing
"x"create; error if the file already exists
"r+"read and write an existing file
add "b"binary mode: "rb", "wb" — bytes, no encoding
Python
with open("log.txt", "w", encoding="utf-8") as f:
    f.write("started\n")

with open("log.txt", "a", encoding="utf-8") as f:
    f.write("still running\n")

with open("log.txt", encoding="utf-8") as f:
    print(f.read(), end="")

try:
    with open("log.txt", "x", encoding="utf-8") as f:
        f.write("never happens")
except FileExistsError as e:
    print("refused:", e.filename)
Output
started
still running
refused: log.txt

Binary files#

Images, PDFs, zip files and anything that isn't text must be opened in binary mode. You read and write bytes objects instead of strings:

Python
data = bytes([0x89, 0x50, 0x4E, 0x47])      # first bytes of a PNG file
with open("sample.bin", "wb") as f:
    f.write(data)

with open("sample.bin", "rb") as f:
    header = f.read(4)
print(header, header[1:4].decode("ascii"))

# copy a file in chunks (works for any size)
with open("sample.bin", "rb") as src, open("copy.bin", "wb") as dst:
    while chunk := src.read(64 * 1024):
        dst.write(chunk)
Output
b'\x89PNG' PNG

For simple copies, shutil.copyfile("a", "b") does this for you.

Paths with pathlib#

Hard-coding "folder/file.txt" breaks on Windows, and string-gluing paths is error-prone. The modern approach is pathlib.Path:

Python
from pathlib import Path

data_dir = Path("data")
data_dir.mkdir(exist_ok=True)

report = data_dir / "report.txt"           # "/" joins paths on every OS
report.write_text("Total: 42\n", encoding="utf-8")

print(report.read_text(encoding="utf-8"), end="")
print(report.name, report.stem, report.suffix, report.parent)
print(report.exists(), report.is_file(), report.stat().st_size)

for path in sorted(data_dir.glob("*.txt")):
    print("found", path)
Output
Total: 42
report.txt report .txt data
True True 10
found data/report.txt

Path.read_text() and Path.write_text() open, read/write and close in one call — perfect for small files. You'll tour more of pathlib in the standard library lesson.

Handling missing files#

Opening a file that doesn't exist raises FileNotFoundError. Handle it where it makes sense:

Python
from pathlib import Path

def load_settings(path):
    try:
        return Path(path).read_text(encoding="utf-8")
    except FileNotFoundError:
        print(f"{path} not found, using defaults")
        return ""

load_settings("missing.ini")
Output
missing.ini not found, using defaults

PermissionError and IsADirectoryError are the other common ones; all three inherit from OSError.

Context managers: what with really does#

A context manager is any object that defines setup and teardown around a block. with calls its __enter__ method at the start and guarantees its __exit__ runs at the end — whether the block finishes normally, returns early, or raises an exception. Without with, you'd need this:

Python
f = open("notes.txt", "w", encoding="utf-8")
try:
    f.write("hello\n")
finally:
    f.close()          # must run even if write() fails
print(f.closed)
Output
True

Files aren't the only context managers. You'll meet locks (threading.Lock), database connections, temporary directories, and more. Some useful ones from the standard library:

Python
import tempfile
from contextlib import suppress
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:          # deleted afterwards
    p = Path(tmp) / "scratch.txt"
    p.write_text("temporary", encoding="utf-8")
    print(p.read_text(encoding="utf-8"))
print(p.exists())

with suppress(FileNotFoundError):                    # ignore one error type
    Path("does-not-exist.txt").unlink()
print("carried on")
Output
temporary
False
carried on

Writing your own context manager#

The easiest way is the @contextmanager decorator from contextlib: write a generator function, put setup before yield and teardown after it (inside try/finally so it always runs):

Python
import time
from contextlib import contextmanager


@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield                                  # the with-block runs here
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.2f}s")


with timer("sleeping"):
    time.sleep(0.2)
Output
sleeping: 0.20s

You can also write a class with __enter__ and __exit__ methods — you'll see that style in the magic methods lesson.

Worked example: summarising a log file#

Python
from collections import Counter
from pathlib import Path

Path("app.log").write_text(
    "2026-09-30 10:00:01 INFO  server started\n"
    "2026-09-30 10:00:05 WARN  slow response 2.3s\n"
    "2026-09-30 10:01:17 ERROR database timeout\n"
    "2026-09-30 10:02:00 INFO  request ok\n"
    "2026-09-30 10:02:09 ERROR database timeout\n",
    encoding="utf-8",
)

levels = Counter()
errors = []
with open("app.log", encoding="utf-8") as f:
    for line in f:
        date, time_, level, message = line.split(maxsplit=3)
        levels[level] += 1
        if level == "ERROR":
            errors.append(f"{time_} {message.strip()}")

with open("summary.txt", "w", encoding="utf-8") as out:
    for level, n in levels.most_common():
        out.write(f"{level:<6}{n}\n")
    out.write("\nErrors:\n" + "\n".join(errors) + "\n")

print(Path("summary.txt").read_text(encoding="utf-8"), end="")
Output
INFO  2
ERROR 2
WARN  1

Errors:
10:01:17 database timeout
10:02:09 database timeout

Common mistakes#

  • Forgetting encoding="utf-8" — works on your machine, breaks on someone else's.
  • Opening with "w" when you meant "a" — the old content is gone instantly.
  • Not using with — files may stay open, and written data may not be flushed.
  • Relative paths are relative to the current working directory, not to the script. Use Path(__file__).parent / "data.txt" to locate files next to your script.
  • Reading huge files with read() — iterate line by line instead.

What's next#

Files fail in many ways — missing, locked, malformed. Next you'll learn to handle failures properly with exceptions, and to define your own.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Why is with open("data.txt") as f: preferred over f = open("data.txt")?

  2. 2.What does opening a file with mode "w" do if the file already exists?

  3. 3.What is the most memory-efficient way to process a 10 GB log file line by line?

Finished reading?

Mark this lesson complete to track your progress.