Python File Handling
This article covers Python’s file read/write operations, including the basics of open(), file modes, encoding handling, and the modern pathlib approach to path manipulation.
File Operation Basics
Python opens files with the open() function, which returns a file object. You then use that object to read or write. When you are done, you must close the file to release system resources.
# Manual close (not recommended — easy to forget)
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
f.close()
# Recommended: with statement closes the file automatically
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# f.close() is called automatically when the with block exitsFile Open Modes
| Mode | Meaning |
|---|---|
r | Read-only (default); raises an error if the file does not exist |
w | Write-only; creates the file if it does not exist; truncates it if it does |
a | Append; creates the file if absent; moves the pointer to the end if present |
x | Exclusive creation; raises an error if the file already exists (Python 3+) |
r+ | Read and write; raises an error if the file does not exist |
b | Binary mode (combined with r/w/a, e.g. rb, wb) |
t | Text mode (default; mutually exclusive with b) |
# Write to a file
with open("log.txt", "w", encoding="utf-8") as f:
f.write("Line one\n")
f.write("Line two\n")
# Append to a file
with open("log.txt", "a", encoding="utf-8") as f:
f.write("Line three\n")
# Open two files simultaneously (read source, write destination)
with open("source.txt", "r", encoding="utf-8") as src, \
open("dest.txt", "w", encoding="utf-8") as dst:
dst.write(src.read())Reading Methods
with open("data.txt", "r", encoding="utf-8") as f:
# Read the entire file into a string at once
content = f.read()
# Read one line (including the trailing \n)
line = f.readline()
# Read all lines and return a list
lines = f.readlines() # ['Line one\n', 'Line two\n', ...]
# Recommended: iterate line by line (does not load the whole file; suitable for large files)
with open("large.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.rstrip()) # rstrip() removes the trailing newlineBinary Mode
Non-text files such as images, videos, and audio must be opened in binary mode (b). Do not specify encoding in binary mode:
# Copy a binary file
with open("photo.jpg", "rb") as src, open("copy.jpg", "wb") as dst:
while True:
chunk = src.read(4096) # Read 4 KB at a time
if not chunk:
break
dst.write(chunk)
# More concise alternative
import shutil
shutil.copyfile("photo.jpg", "copy.jpg")Encoding Handling
On Windows,
open() without an explicit encoding defaults to the system encoding (gbk/cp936), which can cause garbled output. Always specify encoding="utf-8" explicitly.# Read a UTF-8 file
with open("utf8.txt", "r", encoding="utf-8") as f:
print(f.read())
# Read a legacy GBK-encoded file
with open("gbk.txt", "r", encoding="gbk") as f:
content = f.read()
# Specify encoding when writing
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello, World!\n")
# Error handling for bytes that cannot be decoded
with open("mixed.txt", "r", encoding="utf-8", errors="ignore") as f:
content = f.read() # Skip bytes that cannot be decoded
# errors="replace": replace undecodable bytes with ?
with open("mixed.txt", "r", encoding="utf-8", errors="replace") as f:
content = f.read()File Pointer Control
with open("data.txt", "rb") as f:
# tell(): return the current pointer position (in bytes)
print(f.tell()) # 0
f.read(5)
print(f.tell()) # 5
# seek(offset, whence)
f.seek(0) # Move to the beginning (equivalent to f.seek(0, 0))
f.seek(0, 2) # Move to the end (whence=2 means relative to the end)
print(f.tell()) # Total file size in bytes
f.seek(-3, 2) # Move 3 bytes back from the end
print(f.read().decode("utf-8")) # Last 3 bytes of contentpathlib — Modern Path Operations
pathlib.Path is the recommended way to work with file system paths in Python 3.4+, offering a more intuitive interface than os.path:
from pathlib import Path
# Create path objects
data_dir = Path("data")
config = Path("/etc/app/config.yaml")
# Path joining (/ operator)
log_file = data_dir / "logs" / "app.log"
# Path attributes
p = Path("/home/user/docs/readme.txt")
print(p.name) # readme.txt
print(p.stem) # readme
print(p.suffix) # .txt
print(p.parent) # /home/user/docs
# Existence checks
print(p.exists()) # Whether the path exists
print(p.is_file()) # Whether it is a file
print(p.is_dir()) # Whether it is a directory
# Convenient read/write (manages open/close automatically)
output = Path("output.txt")
output.write_text("Hello, World!", encoding="utf-8")
text = output.read_text(encoding="utf-8")
# Create directories (parents=True creates intermediate directories)
Path("a/b/c").mkdir(parents=True, exist_ok=True)
# Delete
output.unlink(missing_ok=True) # Delete a file
Path("empty_dir").rmdir() # Delete an empty directory
# Iterate over a directory
for f in Path(".").iterdir():
if f.is_file():
print(f.name)
# Glob pattern matching
for py in Path("src").glob("**/*.py"): # Recursive search
print(py)Modifying File Contents (Two Approaches)
File content cannot be modified in-place on disk (writes are overwrite-based). Two common strategies:
from pathlib import Path
# Approach 1: Read everything into memory, modify, then write back (suitable for small files)
path = Path("db.txt")
content = path.read_text(encoding="utf-8")
path.write_text(content.replace("old_value", "new_value"), encoding="utf-8")
# Approach 2: Read line by line and write to a temp file (suitable for large files)
import shutil, os
src = Path("db.txt")
tmp = Path(".db.txt.swap")
with src.open("r", encoding="utf-8") as r, \
tmp.open("w", encoding="utf-8") as w:
for line in r:
w.write(line.replace("old_value", "new_value"))
tmp.replace(src) # Atomic replacement (equivalent to os.replace)Practical Example: Tailing a Log File
import time
from pathlib import Path
def tail_log(path: Path) -> None:
"""Similar to tail -f: continuously output new lines appended to a log file."""
with path.open("rb") as f:
f.seek(0, 2) # Jump to the end of the file
while True:
line = f.readline()
if line:
print(line.decode("utf-8"), end="")
else:
time.sleep(0.5)
# tail_log(Path("access.log"))Last updated on