Codetail

Article 12 of 15

File I/O

Reading, writing, context managers.

16 min read

Opening and reading files

open() returns a file object. Always use it as a context manager with with so the file is closed automatically, even if an exception occurs.

Python
1# Read the entire file as one string
2with open("notes.txt", "r") as f:
3 content = f.read()
4 print(content)
5
6# Read line by line, memory-efficient for large files
7with open("notes.txt") as f: # "r" is the default mode
8 for line in f:
9 print(line, end="") # lines already include \n
10
11# Read all lines into a list
12with open("notes.txt") as f:
13 lines = f.readlines() # ['line 1\n', 'line 2\n', ...]
14
15# Read one line at a time
16with open("notes.txt") as f:
17 first = f.readline() # 'line 1\n'

Encoding

Always specify encoding="utf-8" explicitly. The default depends on the operating system and can vary between machines, causing silent data corruption on non-ASCII characters.

Python
1# Explicit encoding, always do this
2with open("data.txt", encoding="utf-8") as f:
3 content = f.read()
4
5# Common encodings
6# utf-8 universal, handles all Unicode
7# utf-8-sig UTF-8 with BOM (common in Windows-generated CSVs)
8# latin-1 legacy Western European files

Each mode controls what operations are allowed and how the file is opened.

File Mode Explorer
Pick a mode
pick a mode above

Writing files

Mode "w" creates the file if it does not exist and overwrites it if it does. Mode "a" appends to the end, preserving existing content. Both create the file if absent.

Python
1# Write, creates or overwrites
2with open("output.txt", "w", encoding="utf-8") as f:
3 f.write("Hello, world!\n")
4 f.write("Second line\n")
5
6# writelines, no automatic newlines added
7lines = ["one\n", "two\n", "three\n"]
8with open("output.txt", "w", encoding="utf-8") as f:
9 f.writelines(lines)
10
11# Append, adds to end without truncating
12with open("log.txt", "a", encoding="utf-8") as f:
13 f.write("2024-01-15 server started\n")

Writing structured data

Python
1# Write a table of data cleanly
2headers = ["name", "score", "grade"]
3rows = [
4 ["Alice", "92", "A"],
5 ["Bob", "78", "C+"],
6 ["Carol", "85", "B"],
7]
8
9with open("results.txt", "w", encoding="utf-8") as f:
10 f.write("\t".join(headers) + "\n")
11 for row in rows:
12 f.write("\t".join(row) + "\n")

Binary files

Add "b" to the mode for binary files. The file object reads and writes bytes instead of strings. No encoding conversion happens.

Python
1# Copy a file byte-for-byte
2with open("image.png", "rb") as src:
3 with open("copy.png", "wb") as dst:
4 dst.write(src.read())
5
6# Read in chunks, for large files
7with open("large.bin", "rb") as f:
8 while chunk := f.read(8192): # walrus operator, Python 3.8+
9 process(chunk)

pathlib

pathlib.Path is the modern way to work with file paths. It is object-oriented, cross-platform, and far more readable than string concatenation or os.path. Prefer it for all new code.

Python
1from pathlib import Path
2
3p = Path("/home/user/documents/report.pdf")
4
5# Parts and properties
6print(p.name) # report.pdf
7print(p.stem) # report
8print(p.suffix) # .pdf
9print(p.parent) # /home/user/documents
10
11# Building paths with /
12base = Path("/home/user")
13config = base / "config" / "settings.json"
14print(config) # /home/user/config/settings.json
15
16# Current directory and relatives
17here = Path.cwd()
18home = Path.home()
19sibling = Path(__file__).parent / "data.csv"

Common operations

Python
1from pathlib import Path
2
3p = Path("data/output.txt")
4
5# Check existence and type
6print(p.exists()) # True/False
7print(p.is_file()) # True if it's a regular file
8print(p.is_dir()) # True if it's a directory
9
10# Create directories
11Path("logs/2024").mkdir(parents=True, exist_ok=True)
12
13# Read and write directly, no open() needed
14p.write_text("hello\n", encoding="utf-8")
15content = p.read_text(encoding="utf-8")
16
17p.write_bytes(b"\x00\x01\x02")
18data = p.read_bytes()
19
20# Glob for files
21for csv_file in Path("data").glob("*.csv"):
22 print(csv_file.name)
23
24# Recursive glob
25for py_file in Path(".").rglob("*.py"):
26 print(py_file)

File operations

Python
1from pathlib import Path
2import shutil
3
4src = Path("report.txt")
5dst = Path("archive/report_backup.txt")
6
7# Rename / move
8src.rename(dst) # moves within the same filesystem
9shutil.move(str(src), str(dst)) # works across filesystems
10
11# Copy
12shutil.copy2(src, dst) # copy with metadata
13
14# Delete
15src.unlink() # delete file
16src.unlink(missing_ok=True) # no error if absent (Python 3.8+)
17
18Path("empty_dir").rmdir() # delete empty dir
19shutil.rmtree("dir_tree") # delete directory and all contents

JSON files

Python's json module serializes Python objects to JSON strings and back. The four functions you need: dump (to file), dumps (to string), load (from file), loads (from string).

Python
1import json
2
3# Write to a file
4config = {"host": "localhost", "port": 5432, "debug": False}
5with open("config.json", "w", encoding="utf-8") as f:
6 json.dump(config, f, indent=2)
7
8# Read from a file
9with open("config.json", encoding="utf-8") as f:
10 loaded = json.load(f)
11print(loaded["port"]) # 5432
12
13# Serialize to/from strings (e.g. for HTTP responses)
14json_str = json.dumps(config, indent=2)
15print(type(json_str)) # <class 'str'>
16
17back = json.loads(json_str)
18print(type(back)) # <class 'dict'>

Type mapping

Python
1# Python → JSON
2# dict → object {}
3# list → array []
4# tuple → array [] (round-trips as list)
5# str → string ""
6# int → number
7# float → number
8# True → true
9# False → false
10# None → null
11
12data = {"items": (1, 2, 3), "flag": True, "value": None}
13print(json.dumps(data))
14# {"items": [1, 2, 3], "flag": true, "value": null}
15
16# json cannot serialize: sets, datetimes, custom classes
17import datetime
18# json.dumps(datetime.date.today()) # TypeError
19
20# Fix: convert manually or use a custom encoder
21print(json.dumps(datetime.date.today(), default=str))
22# "2024-01-15"

Useful options

Python
1data = {"z": 3, "a": 1, "m": 2}
2
3# sort_keys, deterministic output (good for testing and diffs)
4print(json.dumps(data, sort_keys=True))
5# {"a": 1, "m": 2, "z": 3}
6
7# indent, human-readable formatting
8print(json.dumps(data, indent=2))
9# {
10# "z": 3,
11# "a": 1,
12# "m": 2
13# }
14
15# ensure_ascii=False, keep Unicode characters as-is
16print(json.dumps({"city": "Zürich"}, ensure_ascii=False))
17# {"city": "Zürich"}

See how Python objects serialize to JSON and back, with different indent settings.

JSON Explorer
Pick an object
indent
Python object
{
  "name": "Alice",
  "age": 30,
  "admin": True,
  "tags": [
    "python",
    "backend"
  ]
}
json.dumps(obj, indent=2)
{
  "name": "Alice",
  "age": 30,
  "admin": true,
  "tags": [
    "python",
    "backend"
  ]
}
json.loads(json_string) → back to Python dict

CSV files

Python's csv module handles quoting, escaping, and different delimiters correctly. Do not split CSV lines on commas manually, quoted fields with embedded commas will break it.

Python
1import csv
2
3# Read with csv.reader, rows are lists of strings
4with open("scores.csv", encoding="utf-8") as f:
5 reader = csv.reader(f)
6 header = next(reader) # skip the header row
7 for row in reader:
8 name, score = row[0], int(row[1])
9 print(f"{name}: {score}")
10
11# Read with DictReader, rows are dicts keyed by header
12with open("scores.csv", encoding="utf-8") as f:
13 for row in csv.DictReader(f):
14 print(row["name"], row["score"]) # row is a dict

Writing CSV

Python
1import csv
2
3rows = [
4 ["Alice", 92, "A"],
5 ["Bob", 78, "C+"],
6 ["Carol", 85, "B"],
7]
8
9# csv.writer handles quoting and newlines
10with open("results.csv", "w", newline="", encoding="utf-8") as f:
11 writer = csv.writer(f)
12 writer.writerow(["name", "score", "grade"]) # header
13 writer.writerows(rows) # all data rows
14
15# DictWriter, write dicts
16students = [
17 {"name": "Alice", "score": 92},
18 {"name": "Bob", "score": 78},
19]
20with open("students.csv", "w", newline="", encoding="utf-8") as f:
21 writer = csv.DictWriter(f, fieldnames=["name", "score"])
22 writer.writeheader()
23 writer.writerows(students)

Always pass newline="" when opening a file for csv.writer. Without it, Windows adds an extra blank line between each row.