|$ curl https://forge-ai.dev/api/markdown?path=docs/python/file-io
$cat docs/python-—-file-i/o.md
updated Recently·14 min read·published

Python — File I/O

PythonIntermediate
Introduction

File I/O is a fundamental part of nearly every Python program. Python provides several layers of abstraction for working with files: the built-in open() function, the pathlib module for modern path handling, and modules like tempfile and shutil for specialized operations.

open() Modes

The open() function takes a mode string that controls how the file is opened. The mode combines a primary access mode with optional modifiers.

modes.py
Python
1# Primary access modes:
2# "r" — read (default). File must exist.
3# "w" — write. Creates file or truncates existing.
4# "a" — append. Creates file or writes at end.
5# "x" — exclusive create. Fails if file exists.
6# "r+" — read + write. File must exist.
7# "w+" — read + write. Creates or truncates.
8# "a+" — read + append. Creates or writes at end.
9
10# Modifiers (appended to primary mode):
11# "b" — binary mode (e.g. "rb", "wb")
12# "t" — text mode (default, e.g. "rt", "wt")
13
14# Examples:
15open("file.txt", "r") # text read (default)
16open("file.txt", "w") # text write (truncate)
17open("file.txt", "a") # text append
18open("file.txt", "x") # exclusive create (raises FileExistsError)
19open("file.bin", "rb") # binary read
20open("file.bin", "wb") # binary write
21
22# r+ vs w+ vs a+:
23# r+: file must exist, write at current position
24# w+: always creates new file (or truncates)
25# a+: always writes at end, regardless of seek
26# (OS-level append — atomic on some systems)
27
28# Exclusive create (x) is useful for lock files:
29try:
30 with open("lock.pid", "x") as f:
31 f.write(str(os.getpid()))
32except FileExistsError:
33 print("another instance is running")

info

Text mode ("t") is the default under the hood. You almost never need to write "rt" — just "r" suffices. The "b" modifier is the important one when you need binary access.
Reading Files
reading.py
Python
1# Read entire file as a single string
2with open("data.txt") as f:
3 content = f.read()
4
5# Read at most N characters
6with open("data.txt") as f:
7 chunk = f.read(1024) # first 1024 chars
8
9# Read one line
10with open("data.txt") as f:
11 line = f.readline() # "first line\n"
12 second = f.readline() # "second line\n"
13
14# Read all lines into a list
15with open("data.txt") as f:
16 lines = f.readlines() # ["line1\n", "line2\n", ...]
17
18# Best: iterate over lines (memory-efficient)
19with open("data.txt") as f:
20 for line in f:
21 print(line.rstrip("\n")) # process one line at a time
22
23# Read lines with iteration + index
24with open("data.txt") as f:
25 for i, line in enumerate(f, 1):
26 if "ERROR" in line:
27 print(f"line {i}: {line}", end="")
28
29# Read until a delimiter
30with open("data.txt") as f:
31 record = f.read().split("\n---\n") # split on custom delimiter

best practice

Use for line in f: (iteration) over .readlines() for large files. Iteration reads lazily one line at a time, while readlines() loads the entire file into memory at once.
Writing Files
writing.py
Python
1# Write a string
2with open("output.txt", "w") as f:
3 f.write("Hello, World!\n")
4
5# Write multiple strings (no newlines added)
6with open("output.txt", "w") as f:
7 f.writelines([
8 "line 1\n",
9 "line 2\n",
10 "line 3\n",
11 ])
12
13# writelines with list of strings — no separator added
14# This is equivalent to:
15# for line in lines: f.write(line)
16
17# Appending to existing file
18with open("log.txt", "a") as f:
19 f.write(f"[{datetime.now()}] request received\n")
20
21# Print to file (convenient for formatted output)
22with open("output.txt", "w") as f:
23 print("Hello, World!", file=f)
24 print(f"Value: {42}", file=f)
25
26# Flushing — force write buffer to disk
27with open("output.txt", "w") as f:
28 f.write("critical data")
29 f.flush() # ensure data is written to disk
30 # File stays open — useful before long operations
31
32# Buffering control:
33# buffering=0 → unbuffered (binary mode only)
34# buffering=1 → line-buffered (text mode only)
35# buffering=N → buffer N bytes before flushing
36# Default: 4096 or 8192 depending on system
37with open("data.bin", "wb", buffering=0) as f:
38 f.write(b"immediate write, no buffer")

info

writelines() does NOT add newline characters. You must include them in each string. Despite the name, it's really "write many strings" — the "lines" naming is historical.
Binary Files & Seek/Tell

Binary mode ("b") reads and writes bytes objects instead of str. No encoding conversion or newline translation is performed.

binary.py
Python
1# Reading binary files
2with open("image.jpg", "rb") as f:
3 header = f.read(4) # first 4 bytes
4 if header == b"\xff\xd8\xff\xe0":
5 print("JPEG detected")
6
7# Writing binary data
8with open("output.bin", "wb") as f:
9 f.write(b"\x00\x01\x02\x03")
10 f.write(bytes([0x04, 0x05, 0x06]))
11
12# seek() — move to a position (offset, whence):
13# whence=0: relative to file start (default)
14# whence=1: relative to current position
15# whence=2: relative to file end
16with open("data.bin", "rb") as f:
17 f.seek(0, 2) # seek to end
18 size = f.tell() # get current position = file size
19 print(f"file size: {size} bytes")
20
21 f.seek(0) # seek to beginning
22 f.seek(100) # skip to byte 100
23 data = f.read(50) # read bytes 100-149
24
25 f.seek(-10, 2) # 10 bytes before end
26 tail = f.read() # last 10 bytes
27
28 f.seek(10, 1) # skip 10 bytes forward
29
30# tell() — get current position (bytes from start)
31with open("data.bin", "rb") as f:
32 print(f.tell()) # 0
33 f.read(50)
34 print(f.tell()) # 50
35
36# Seek in text mode — only seek(0) or seek(0, 2) allowed
37with open("data.txt", "r") as f:
38 f.seek(0) # OK — go to start
39 f.seek(0, 2) # OK — go to end
40 # f.seek(50) # ERROR in text mode (variable-width encoding)
41 # f.seek(-10, 1) # ERROR in text mode

best practice

seek() in text mode is fragile because multi-byte encodings (UTF-8) make byte offsets unpredictable. Use binary mode if you need random access positioning, or read line-by-line and track position yourself.
pathlib — Modern Approach

The pathlib module provides an object-oriented interface for filesystem paths, with convenience methods for common I/O operations.

pathlib.py
Python
1from pathlib import Path
2
3# Reading and writing text (in one call)
4content = Path("data.txt").read_text(encoding="utf-8")
5Path("output.txt").write_text("hello world", encoding="utf-8")
6
7# Reading and writing bytes
8data = Path("image.jpg").read_bytes()
9Path("copy.jpg").write_bytes(data)
10
11# Path construction and properties
12p = Path("/usr/local/bin/python3")
13p.parent # → /usr/local/bin
14p.name # → python3
15p.stem # → python (no suffix)
16p.suffix # → (empty — no .ext)
17p.suffixes # → [] (list of extensions)
18
19p = Path("archive.tar.gz")
20p.suffix # → .gz
21p.suffixes # → [".tar", ".gz"]
22p.stem # → archive.tar
23p.with_suffix(".bz2") # → archive.tar.bz2
24
25p = Path("data.txt")
26p.exists() # → bool
27p.is_file()
28p.is_dir()
29p.stat().st_size # file size in bytes
30p.stat().st_mtime # last modified timestamp
31
32# Absolute vs relative
33Path("data.txt").resolve() # → /Users/me/data.txt
34Path("data.txt").absolute() # → /Users/me/data.txt
35
36# Iterate directory
37for child in Path(".").iterdir():
38 print(child.name, "dir?" if child.is_dir() else "file")
39
40# Glob patterns
41for py in Path(".").glob("**/*.py"):
42 print(py)
43
44# Relative paths
45p = Path("/a/b/c/d.txt")
46p.relative_to("/a/b") # → PosixPath("c/d.txt")
47
48# Join paths with /
49config = Path.home() / ".config" / "myapp" / "config.json"

info

pathlib is the modern, preferred approach for file I/O in Python 3.6+. The / operator for path joining reads much cleaner than nested os.path.join() calls. Prefer it over os.path for new code.
tempfile Module

The tempfile module creates temporary files and directories that are automatically cleaned up, with secure random names.

tempfile.py
Python
1import tempfile
2from pathlib import Path
3
4# Temporary file — auto-deleted on close
5with tempfile.TemporaryFile() as f:
6 f.write(b"temporary data")
7 f.seek(0)
8 print(f.read()) # b"temporary data"
9# File deleted after leaving 'with' block
10
11# Named temporary file — visible in filesystem
12with tempfile.NamedTemporaryFile(
13 suffix=".txt",
14 prefix="tmp_",
15 delete=True, # delete on close (default)
16) as f:
17 print(f.name) # e.g. /tmp/tmp_abc123.txt
18 f.write(b"hello")
19# File deleted (unless delete=False)
20
21# Permanent temp file (not auto-deleted)
22f = tempfile.NamedTemporaryFile(delete=False)
23f.write(b"persistent data")
24path = f.name
25f.close()
26# Must delete manually later:
27Path(path).unlink()
28
29# Temporary directory
30with tempfile.TemporaryDirectory() as tmpdir:
31 path = Path(tmpdir) / "data.txt"
32 path.write_text("hello")
33 print(path.read_text()) # "hello"
34# Entire directory tree deleted after 'with'
35
36# Get system temp directory
37print(tempfile.gettempdir()) # e.g. /tmp
38print(tempfile.gettempprefix()) # e.g. tmp
39
40# Secure temp file (less predictable names)
41# TemporaryFile/NamedTemporaryFile already use
42# secure random names by default

best practice

Always use TemporaryFile() or NamedTemporaryFile() as context managers. If you set delete=False, you are responsible for cleaning up with Path(path).unlink() — otherwise temp files accumulate.
File & Directory Operations
file_ops.py
Python
1from pathlib import Path
2import os
3import shutil
4
5# ── Creating ──
6Path("new_dir").mkdir() # single directory
7Path("a/b/c").mkdir(parents=True) # recursive (like mkdir -p)
8os.makedirs("a/b/c", exist_ok=True) # legacy equivalent
9
10# ── Removing ──
11Path("empty_dir").rmdir() # directory must be empty
12Path("file.txt").unlink() # delete file (no-op if missing)
13Path("file.txt").unlink(missing_ok=True) # Python 3.8+ — no error
14
15# ── Rename / Move ──
16Path("old.txt").rename("new.txt")
17Path("old.txt").replace("new.txt") # overwrites existing
18
19# ── Copy (shutil) ──
20shutil.copy("src.txt", "dst.txt") # file → file
21shutil.copy("src.txt", "backup/") # file → directory
22shutil.copy2("src.txt", "dst.txt") # preserves metadata
23shutil.copytree("src_dir", "dst_dir") # recursive directory copy
24shutil.copytree("src", "dst", dirs_exist_ok=True) # Python 3.8+
25
26# ── Move (shutil) ──
27shutil.move("src.txt", "dest/subdir/") # move file into directory
28shutil.move("old_dir", "new_dir") # rename directory
29
30# ── Delete tree ──
31shutil.rmtree("dir_to_delete") # delete directory tree
32
33# ── File info (stat) ──
34stat = Path("file.txt").stat()
35stat.st_size # size in bytes
36stat.st_mode # permission bits
37stat.st_mtime # last modified (timestamp)
38stat.st_ctime # creation time (Unix) / metadata change (Windows)
39stat.st_atime # last access time
40
41# ── Glob / Listing ──
42for p in Path(".").glob("*.txt"): # current dir only
43 print(p)
44
45for p in Path(".").rglob("*.py"): # recursive (**/*.py)
46 print(p)
47
48for p in Path(".").iterdir(): # all entries (non-recursive)
49 pass
50
51# ── os.path legacy (avoid in new code) ──
52os.path.exists("file.txt")
53os.path.isfile("file.txt")
54os.path.isdir("dir")
55os.path.join("a", "b", "c.txt")
56os.path.splitext("file.txt") # → ("file", ".txt")

info

Prefer pathlib over os.path for all new code. It provides method chaining, operator overloading (/ for join), and a cleaner API. Fall back to shutil for copy/move/delete-tree operations that pathlib doesn't cover.
Best Practices
best_practices.py
Python
1# 1. Always use context managers ('with' statement)
2# BAD:
3f = open("data.txt")
4data = f.read()
5f.close() # easy to forget → resource leak
6
7# GOOD:
8with open("data.txt") as f:
9 data = f.read()
10# auto-closed, even on exception
11
12# 2. Specify encoding explicitly
13# BAD (platform-dependent default):
14with open("data.txt") as f:
15 ...
16
17# GOOD (portable):
18with open("data.txt", encoding="utf-8") as f:
19 ...
20
21# 3. Prefer pathlib for new code
22# BAD:
23import os
24content = open(os.path.join("dir", "file.txt")).read()
25
26# GOOD:
27from pathlib import Path
28content = Path("dir/file.txt").read_text(encoding="utf-8")
29
30# 4. Handle newline differences portably
31# Python normalizes to '\n' in text mode by default
32# On Windows, '\r\n' → '\n' (reading) and vice versa (writing)
33with open("data.txt", newline="") as f:
34 # newline="" — disable translation, read raw lines
35 for line in f:
36 print(repr(line)) # keeps original line endings
37
38# 5. EAFP for file operations
39# LBYL — race condition (file could be deleted between check and open)
40if Path("data.txt").exists():
41 with open("data.txt") as f: # still might fail!
42 ...
43
44# EAFP — Pythonic
45try:
46 with open("data.txt") as f:
47 data = f.read()
48except FileNotFoundError:
49 data = ""
50
51# 6. Use file-like objects for testing / mocking
52from io import StringIO, BytesIO
53
54# StringIO acts like a text file in memory
55buf = StringIO()
56buf.write("hello")
57buf.seek(0)
58print(buf.read()) # "hello"
59
60# BytesIO acts like a binary file in memory
61bbuf = BytesIO()
62bbuf.write(b"\x00\x01\x02")
63bbuf.seek(0)
64print(bbuf.read()) # b"\x00\x01\x02"
65
66# Useful for testing — avoid real files in unit tests
67def process(f):
68 return f.read().upper()
69
70assert process(StringIO("hello")) == "HELLO"

best practice

File-like objects (StringIO / BytesIO) are invaluable for testing. They implement the same interface as real file objects, so you can pass them to any function that expects a file — no temp files needed in unit tests.
$Blueprint — Engineering Documentation·Section ID: PYTHON-FIO·Revision: 1.0