Part of Python for Beginners

File Handling (read, write, append, context manager, pathlib) - Python Tutorial for Beginners #15

Sandy LaneSandy Lane

Video: File Handling (read, write, append, context manager, pathlib) - Python Tutorial for Beginners #15 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Python File Handling: open, read, write, with

with open(path, mode) as f: is the canonical pattern. Modes: "r" read, "w" write (truncate), "a" append, "b" binary, "+" read/write. The with block guarantees the file closes even on exception.

Reading and writing files is one of the first "real-world" things you'll do with Python. The pattern is small but worth getting right.

The basic pattern: with open

with open("hello.txt", "w") as f:
  f.write("Hello, World!\n")
  f.write("Python is awesome!\n")

with open(path, mode) as f: opens the file. The with block guarantees it gets closed when the block ends — even if an exception is raised.

Three modes you'll use 95% of the time:

  • "r" — read (default if mode omitted). File must exist.
  • "w" — write. Truncates existing content.
  • "a" — append. Creates if missing; writes to end.

Reading: three ways

# Read everything as one string
with open("hello.txt", "r") as f:
  content = f.read()
print(content)

# Read all lines into a list
with open("hello.txt", "r") as f:
  lines = f.readlines()    # list of strings, with newlines

# Iterate line by line — most memory-efficient
with open("hello.txt", "r") as f:
  for line in f:
    print(line.strip())    # strip removes the trailing \n

For huge files, iterate line-by-line. read() and readlines() load the whole file into memory.

line.strip() is needed because each line includes its trailing \n. Or use line.rstrip("\n") to keep other whitespace.

Writing

with open("output.txt", "w") as f:
  f.write("Line 1\n")
  f.write("Line 2\n")

# Multiple lines at once
with open("output.txt", "w") as f:
  f.writelines(["Line 1\n", "Line 2\n", "Line 3\n"])

f.write(s) writes one string. No automatic newline — you have to add \n.

f.writelines(iterable) writes each string from the iterable. Despite the name, it doesn't add newlines either.

Append vs write

with open("log.txt", "w") as f:
  f.write("First entry\n")    # creates file, writes

with open("log.txt", "w") as f:
  f.write("Second entry\n")   # TRUNCATES, file now has only second entry

with open("log.txt", "a") as f:
  f.write("Third entry\n")    # appends — preserves first

"w" always starts fresh. "a" adds to the end. For logs, always "a".

A log manager

logs = [
  ("INFO", "Application started"),
  ("WARNING", "Low memory detected"),
  ("ERROR", "Failed to connect"),
]

# Write
with open("app.log", "w") as f:
  for level, message in logs:
    f.write(f"[{level}] {message}\n")

# Filter ERRORs
with open("app.log", "r") as f:
  errors = [line.strip() for line in f if "ERROR" in line]

# Count by level
counts = {}
with open("app.log", "r") as f:
  for line in f:
    level = line.split("]")[0].strip("[")
    counts[level] = counts.get(level, 0) + 1

Three patterns in one file: structured write, filtered read, aggregate count. The with block in each opens just for that operation.

Why with matters

Without with, you'd have to remember to close the file:

f = open("file.txt", "r")
content = f.read()
f.close()    # easy to forget — and skipped on exception

If f.read() raises, close() never runs. The file stays open until garbage collection — could be milliseconds, could be much longer. On Windows, an unclosed file blocks deletion. On any OS, leaking file handles will eventually exhaust the OS limit.

with is a context manager — it ensures cleanup. Use it for every file operation.

Modes in detail

Mode Description
"r" read text (default)
"w" write text (truncate)
"a" append text
"r+" read + write (must exist)
"w+" write + read (truncate)
"a+" append + read
"rb", "wb", "ab" same, but binary mode
"x" exclusive create — fails if file exists

For binary files (images, PDFs, etc.) always use b:

with open("image.png", "rb") as f:
  data = f.read()    # bytes, not str

Text mode handles encoding (default UTF-8 in Python 3). Binary mode gives you raw bytes.

Encoding

with open("notes.txt", "r", encoding="utf-8") as f:
  text = f.read()

Always specify encoding="utf-8". Default depends on platform — on Windows it's often cp1252, which silently mangles non-ASCII. Explicit is safer.

For latin1, shift-jis, etc., specify the appropriate name.

File position: seek and tell

with open("file.txt", "r") as f:
  print(f.tell())     # 0 — start
  f.read(5)
  print(f.tell())     # 5

  f.seek(0)           # back to start
  f.read(10)

  f.seek(0, 2)        # seek to end (offset 0 from end-of-file marker 2)

Rarely needed for text. Useful for binary protocols where you need to jump around.

Pathlib: the modern way

from pathlib import Path

p = Path("hello.txt")
p.write_text("Hello!\n")
content = p.read_text()
p.write_bytes(b"\x00\x01")
data = p.read_bytes()

# File operations
p.exists()
p.is_file()
p.unlink()    # delete

pathlib.Path is more ergonomic for most file work. read_text / write_text open, read/write, close — no with needed for one-off reads.

For line-by-line reads of huge files, still use with open(...).

Reading from URLs vs files

# Local
with open("data.json") as f:
  data = json.load(f)

# Remote
import requests
r = requests.get("https://api.example.com/data.json")
data = r.json()

Pattern is the same. We cover JSON in lesson 18 and HTTP requests in lesson 32.

CSV files

import csv

with open("data.csv", "r") as f:
  reader = csv.reader(f)
  for row in reader:
    print(row)    # list of strings

# DictReader for headers
with open("data.csv", "r") as f:
  reader = csv.DictReader(f)
  for row in reader:
    print(row["name"], row["age"])

Use the csv module — don't try to split on commas yourself (commas inside quoted fields will trip you up). Even better: pandas.read_csv() for analysis.

Common stumbles

Forgetting with. File leaks. Always use with.

Wrong mode. "w" overwrites — use "a" if you mean to append.

Forgetting \n. f.write("line") — no newline added. The next write runs on the same line.

Iteration on closed file. Once the with block ends, f is closed. Don't store and reuse outside.

Non-ASCII corruption. Always pass encoding="utf-8" explicitly.

Reading a huge file with .read(). Loads into memory. Iterate line-by-line for big files.

os.path.join everywhere. Use pathlib.Path — cleaner, cross-platform.

Mixing binary and text mode writes. f.write("string") in "wb" mode → TypeError. Bytes mode wants bytes: f.write(b"...").

What's next

Lesson 16: exception handling. try, except, else, finally, raise, custom exception classes.

Recap

with open(path, mode, encoding="utf-8") as f: for guaranteed close. Modes: "r" "w" "a" (text) and "rb" "wb" "ab" (binary). f.read() returns whole file; for line in f: iterates line by line (memory efficient). f.write(s) writes one string (no automatic newline). pathlib.Path for path manipulation and one-shot reads. Always specify encoding for text files.

Next lesson: exception handling.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.