← Back to Course Index
Lesson 5 of 10

File I/O & Context Managers

~20 min · open(), with, pathlib, JSON, CSV

Ref
Primary Source
Official Docs — Reading and Writing Files (section 7.2)

Covers open(), modes, and the with statement. Also see the pathlib docs for modern path handling.

1 — The with Statement (Context Manager)

This is Python's answer to resource management — like Node.js's fs.open() + close, but automatic. The with block guarantees cleanup even if an exception occurs.

JavaScript (Node.js)
const fs = require('fs');

// Old style — manual close
const fd = fs.openSync('file.txt', 'r');
try {
  const content = fs.readFileSync('file.txt', 'utf8');
  console.log(content);
} finally {
  fs.closeSync(fd);
}

// Modern — streams / promises
const content = await fs.promises.readFile(
  'file.txt', 'utf8'
);
Python
# with automatically closes the file
with open("file.txt", "r") as f:
    content = f.read()
# file is closed here — guaranteed

# No with (avoid this)
f = open("file.txt", "r")
content = f.read()
f.close()  # easy to forget / skip on error
💡 Always use with for files

The with statement calls __enter__ on open and __exit__ on close — even if an exception is raised. This is the correct, idiomatic Python pattern. You'll see it everywhere.

2 — File Modes

ModeMeaningFile must exist?
"r"Read (default)Yes — FileNotFoundError otherwise
"w"Write — truncates firstNo — creates it
"a"AppendNo — creates it
"x"Create — fails if existsNo — must NOT exist
"r+"Read + writeYes
"rb", "wb"Binary modeDepends on r/w

3 — Reading & Writing Patterns

# Read entire file as string
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

# Read line by line (memory-efficient for large files)
with open("data.txt", "r") as f:
    for line in f:
        print(line.strip())   # strip() removes trailing \n

# Read all lines into a list
with open("data.txt", "r") as f:
    lines = f.readlines()     # [" line1\n", "line2\n", ...]

# Write
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello, World!\n")
    f.writelines(["line1\n", "line2\n"])

# Append
with open("log.txt", "a") as f:
    f.write(f"Event logged\n")
💡 Always specify encoding

Use encoding="utf-8" explicitly. Without it, Python uses the system default (which can differ between macOS, Windows, Linux). This prevents subtle bugs with non-ASCII characters.

4 — pathlib — Modern Path Handling

pathlib.Path is the modern, OOP way to work with file paths. Think of it as the path module in Node.js — but object-oriented.

from pathlib import Path

# Create a path object
p = Path("data/users.txt")
base = Path.home()          # /Users/rax
cwd = Path.cwd()            # current working directory

# Path operations — use / operator to join (not os.path.join!)
config_file = base / ".config" / "app.json"

# Inspect
p.exists()          # True/False
p.is_file()         # True/False
p.is_dir()          # True/False
p.suffix            # ".txt"
p.stem              # "users"
p.name              # "users.txt"
p.parent            # Path("data")

# Create directories
Path("output/logs").mkdir(parents=True, exist_ok=True)

# Glob (like glob.glob but better)
for py_file in Path(".").glob("**/*.py"):
    print(py_file)

# Read/write directly from Path (Python 3.5+)
text = p.read_text(encoding="utf-8")
p.write_text("new content", encoding="utf-8")

# List directory contents
for item in Path(".").iterdir():
    print(item.name)

5 — JSON & CSV

JSON

JavaScript
// Parse
const obj = JSON.parse(jsonString);

// Stringify
const str = JSON.stringify(obj, null, 2);

// File (Node.js)
const data = JSON.parse(
  fs.readFileSync('data.json', 'utf8')
);
Python
import json

# Parse string → dict
obj = json.loads(json_string)

# Serialize dict → string
s = json.dumps(obj, indent=2)

# Read from file
with open("data.json") as f:
    data = json.load(f)         # load (not loads)

# Write to file
with open("output.json", "w") as f:
    json.dump(data, f, indent=2) # dump (not dumps)

CSV

import csv

# Read CSV
with open("data.csv", newline="") as f:
    reader = csv.DictReader(f)    # rows as dicts using header row
    for row in reader:
        print(row["name"], row["age"])

# Write CSV
with open("output.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "age"])
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30})

6 — Writing Your Own Context Manager

Any class with __enter__ and __exit__ works as a context manager. Or use the simpler @contextmanager decorator:

from contextlib import contextmanager
import time

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

with timer("database query"):
    results = db.query("SELECT * FROM users")
# prints: "database query: 0.042s"

🧠 Quiz

1. What guarantees does the with statement provide?

2. What is the difference between json.load() and json.loads()?

3. How do you join two paths with pathlib?

4. Opening a file with mode "w" on an existing file will…

0/4

Questions answered correctly.