Most programs read or write files at some point. Python gives you a clean trio: pathlib for paths, open() with the with statement for safe file handles, and the json and csv modules for structured data. This lesson covers the patterns you will use in real code.
pathlib for paths
pathlib.Path is the modern way to work with filesystem paths. Paths are objects, not strings, and they have useful methods for reading, writing, joining, and inspecting. Use Path.cwd() for the current directory and Path.home() for the user's home.
from pathlib import Path
p = Path("data") / "users.csv"
print(p.exists())
print(p.suffix) # .csv
print(p.parent) # data
print(p.absolute()) # full path
# List Python files in the current directory
for f in Path(".").glob("*.py"):
print(f)
Reading and writing text
open(path, mode) returns a file object. The with statement closes it for you. Use mode r to read, w to write (truncating), a to append, and x to create exclusively. Always pass encoding="utf-8" for predictable behaviour across platforms.
from pathlib import Path
p = Path("notes.txt")
# Write
p.write_text("Hello, World!\nWelcome to Python.\n", encoding="utf-8")
# Read whole file
print(p.read_text(encoding="utf-8"))
# Read line by line
with p.open(encoding="utf-8") as f:
for line in f:
print(line.strip())
JSON with json.dump and json.load
json.dump writes a Python object to a file. json.load reads one back. Use indent for human-readable output. JSON only supports dict, list, str, int, float, bool, and None. Custom objects need a custom encoder or a to_dict method.
import json
from pathlib import Path
user = {
"name": "Asha",
"age": 28,
"roles": ["admin", "editor"],
"active": True,
"prefs": None,
}
p = Path("user.json")
p.write_text(json.dumps(user, indent=2), encoding="utf-8")
loaded = json.loads(p.read_text(encoding="utf-8"))
print(loaded["name"])
CSV with csv.DictReader and csv.DictWriter
The csv module reads and writes comma-separated values. DictReader maps each row to a dict keyed by the header. DictWriter writes dicts as rows. Use newline="" on open() to keep the csv module happy on Windows.
import csv
from pathlib import Path
p = Path("users.csv")
# Write
with p.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age", "city"])
writer.writeheader()
writer.writerows([
{"name": "Asha", "age": 28, "city": "Pune"},
{"name": "Bala", "age": 31, "city": "Chennai"},
])
# Read
with p.open(encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row["name"], row["age"])
Read and write bytes for binary data
For images, audio, or any non-text data, use mode rb or wb. Path.write_bytes and read_bytes are the one-liner counterparts. The bytes type holds 8-bit values, not Unicode characters.
from pathlib import Path
p = Path("data.bin")
p.write_bytes(b"\x00\x01\x02\x03\xff")
data = p.read_bytes()
print(data[0], data[-1]) # 0 255
Common pitfalls with files and I/O
Three mistakes trip people up the most. First, opening a file without specifying encoding falls back to the platform default, which is cp1252 on Windows and UTF-8 on macOS. Reading a UTF-8 file on Windows without explicit encoding can raise UnicodeDecodeError on accented characters. Always pass encoding="utf-8" to text files. Second, mixing text and binary modes raises TypeError mid-stream. A file opened in text mode expects str, and one in binary mode expects bytes. Decide the mode based on whether the file is human-readable or not. Third, forgetting newline="" on csv files produces blank lines on Windows because the csv module writes its own line endings and they collide with the text-mode translation. The newline="" argument tells open() to leave line endings alone so the csv module can manage them.
Comparison with JavaScript fs and Node streams
If you come from JavaScript, Python's file model is much simpler. Node.js uses an asynchronous, callback-based API (fs.readFile, fs.writeFile) plus the stream API for large files, and every I/O call returns a Promise. Python offers the same async story with aiofiles and asyncio, but the default synchronous API is the workhorse for scripts and small tools. The pathlib API in Python is roughly equivalent to Node's path module: both treat paths as objects with useful methods for joining, resolving, and inspecting. The main difference is the error model. Node surfaces errors via the callback or Promise rejection; Python raises exceptions. Synchronous code in Python tends to be very short because the language optimises for readability over deep nesting.
Performance notes for file operations
Three habits keep file-heavy code fast. First, for huge files, read line by line with for line in f: rather than f.read() into memory; the iterator is lazy and uses constant memory. Second, write in larger chunks rather than many small writes; buffering kicks in and reduces syscalls. Third, for binary data that you need to parse quickly, reach for the struct module to unpack fixed-size records and the mmap module to memory-map very large files. Combine these with the standard tempfile module for safe scratch space and the gzip module for transparent compression, and your scripts can handle gigabyte-scale files without changing the structure of the program.
Summary
Use pathlib for paths, the with statement for safe file handles, json for structured data, and csv for tabular data. Always pass encoding="utf-8" for text and newline="" for csv. The next lesson covers modules, packages, and the venv-based workflow that professional Python projects use.