Python File Handling: The Complete Beginner's Guide

This chapter teaches persistent data workflows: safe file reads/writes, modes, CSV and JSON, robust error handling, binary operations, and modern path work using pathlib.

Chapter 14 of 20 · Intermediate · 45 min · Python Programming Course

Every program you have written so far forgets everything when it stops. File handling gives your programs permanent memory.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Open files safely with with open()
  • Read files using read(), readline(), readlines(), and iteration
  • Write and append content correctly
  • Understand file modes and their risks
  • Work with CSV and JSON files
  • Handle paths safely with pathlib
  • Handle file errors robustly
  • Work with binary files
  • Avoid common file handling mistakes

Estimated time: 45 minutes reading + 25 minutes practice

The open() Function

file = open("example.txt", "r")
content = file.read()
file.close()

This works, but can leak resources if an exception occurs before close().

Always Use with

with open("example.txt", "r") as file:
    content = file.read()

The file closes automatically, even on errors.

File Modes

ModeNameBehavior
`r`ReadFile must exist
`w`WriteTruncates existing file
`a`AppendWrites at end, preserves content
`x`Exclusive createFails if file exists
`b`BinaryUse with other modes (`rb`, `wb`)
`+`Read+WriteCombined read/write modes

Reading Files

read()

with open("poem.txt", "r") as file:
    content = file.read()

readline()

with open("data.txt", "r") as file:
    first_line = file.readline()

readlines()

with open("students.txt", "r") as file:
    lines = file.readlines()

Iteration (recommended default)

with open("students.txt", "r") as file:
    for line in file:
        print(line.strip())

Writing and Appending

with open("output.txt", "w") as file:
    file.write("First line\n")
    file.write("Second line\n")
with open("log.txt", "a") as file:
    file.write("New log entry\n")

Working With CSV

import csv

with open("students.csv", "r", newline="") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row["name"], row["score"])
import csv

rows = [{"name": "Alice", "score": 88}, {"name": "Bob", "score": 75}]
with open("output.csv", "w", newline="") as file:
    writer = csv.DictWriter(file, fieldnames=["name", "score"])
    writer.writeheader()
    writer.writerows(rows)

Working With JSON

import json

data = {"course": "Python", "students": [{"name": "Alice", "score": 88}]}
with open("course_data.json", "w") as file:
    json.dump(data, file, indent=2)
with open("course_data.json", "r") as file:
    loaded = json.load(file)

pathlib - Modern Path Handling

from pathlib import Path

path = Path("data") / "processed" / "output.csv"
print(path.exists())
from pathlib import Path
p = Path("notes.txt")
p.write_text("Hello\n")
print(p.read_text())

Handling File Errors

try:
    with open("missing.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("Permission denied")
except IsADirectoryError:
    print("Path is a directory")

Binary Files

with open("original.jpg", "rb") as source:
    image_data = source.read()

with open("copy.jpg", "wb") as dest:
    dest.write(image_data)

A Complete Working Program

CSV in, analysis, CSV+JSON out:

import csv
import json
from datetime import datetime

def read_student_scores(filepath):
    students = []
    with open(filepath, "r", newline="") as file:
        reader = csv.DictReader(file)
        for row in reader:
            students.append({
                "name": row["name"].strip(),
                "scores": [int(x) for x in row["scores"].split("|")],
                "course": row.get("course", "General"),
            })
    return students

def analyse_students(students):
    results = []
    for s in students:
        avg = sum(s["scores"]) / len(s["scores"])
        grade = "A" if avg >= 90 else "B" if avg >= 80 else "C" if avg >= 70 else "D" if avg >= 60 else "F"
        results.append({
            "name": s["name"],
            "course": s["course"],
            "average": round(avg, 1),
            "grade": grade,
            "passed": avg >= 60,
        })
    return sorted(results, key=lambda x: x["average"], reverse=True)

5 File Handling Mistakes Every Beginner Makes

  1. Opening files without with and leaking handles
  2. Using w when you meant a
  3. Forgetting to strip trailing newlines from lines
  4. Hardcoding path separators instead of pathlib
  5. Reading huge files entirely into memory unnecessarily

Practice Exercises

  • Basic read/write with line numbers and word count
  • CSV inventory value calculator
  • JSON config loader/updater/reset
  • Log analyzer for INFO/WARNING/ERROR
  • Recursive file search utility with pathlib

-> See all Python File Handling exercises with solutions

What Comes Next - Day 15: Exception Handling

Day 15 dives deeper into robust error control:

  • try/except/else/finally
  • specific vs broad exceptions
  • raising exceptions
  • custom exception classes
  • exception chaining
  • exceptions vs return-value signaling

-> Continue to Day 15: Exception Handling

Frequently Asked Questions

What is file handling in Python?

Reading and writing persistent data on disk through file objects.

Why use with for file handling?

It guarantees file closure and prevents leaked resources.

Difference between r, w, and a modes?

r reads existing files, w overwrites, a appends.

Difference between read, readline, readlines?

Entire text, single line, or list of all lines respectively.

How do I work with CSV in Python?

Use the csv module, preferably DictReader/DictWriter.

How do I work with JSON in Python?

Use json.load/json.dump for files and loads/dumps for strings.

Why use pathlib?

Cleaner, cross-platform path operations with object-oriented API.

How do I handle file errors?

Catch specific exceptions like FileNotFoundError, PermissionError, and IsADirectoryError.

Chapter navigation

pathlib — The Modern Way to Handle Paths

The pathlib.Path object replaces all the clunky os.path string concatenation with clean object-oriented APIs.

from pathlib import Path

# Create paths with / operator — readable and cross-platform
base = Path('/var/data')
report = base / '2026' / 'june' / 'report.csv'

# Common operations
print(report.name)       # 'report.csv'
print(report.stem)       # 'report'
print(report.suffix)     # '.csv'
print(report.parent)     # /var/data/2026/june
print(report.parts)      # ('/', 'var', 'data', '2026', 'june', 'report.csv')

# File operations
report.parent.mkdir(parents=True, exist_ok=True)  # Create directories
report.write_text("date,sales
2026-06-01,1500")  # Write entire file
content = report.read_text(encoding='utf-8')       # Read entire file
report.rename(report.parent / 'report_final.csv')  # Rename/move

# Discovery
for csv_file in Path('.').glob('**/*.csv'):         # Recursive glob
    print(csv_file)

# Path properties
path = Path('~/Documents/data.json').expanduser()  # Resolve ~
abs_path = path.resolve()                          # Absolute path
relative = path.relative_to(Path.home())           # Relative to home

Binary File Handling

Not all files are text. Images, PDFs, executables and serialized data are binary.

# Write binary data
with open('image_copy.png', 'wb') as f:      # 'wb' = write binary
    with open('original.png', 'rb') as src:  # 'rb' = read binary
        f.write(src.read())

# Reading in chunks (for large files)
def copy_file_chunked(src_path, dst_path, chunk_size=65536):
    with open(src_path, 'rb') as src, open(dst_path, 'wb') as dst:
        while chunk := src.read(chunk_size):
            dst.write(chunk)

# Struct — pack/unpack binary data (network protocols, file headers)
import struct

# Pack: 2-byte unsigned short + 4-byte float
packed = struct.pack('>Hf', 42, 3.14)        # Big-endian
count, value = struct.unpack('>Hf', packed)  # Unpack back

# BytesIO — in-memory binary file
from io import BytesIO
buffer = BytesIO()
buffer.write(b'hello world')
buffer.seek(0)
print(buffer.read())   # b'hello world'

Working with ZIP and Compressed Files

import zipfile
from pathlib import Path

# Create a zip archive
with zipfile.ZipFile('archive.zip', 'w', compression=zipfile.ZIP_DEFLATED) as zf:
    zf.write('report.csv')                          # Add single file
    for p in Path('data').glob('**/*.json'):
        zf.write(p, arcname=str(p.relative_to('data')))  # Preserve structure

# Read and extract
with zipfile.ZipFile('archive.zip', 'r') as zf:
    print(zf.namelist())                    # List all files
    zf.extractall('output_dir')             # Extract all
    content = zf.read('report.csv')        # Read without extracting

# Check and selectively extract
with zipfile.ZipFile('archive.zip') as zf:
    for info in zf.infolist():
        if info.filename.endswith('.json'):
            zf.extract(info, 'json_only/')

File Watching and Temp Files

import tempfile
import os

# Temporary files — auto-deleted when closed/context exits
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=True) as tmp:
    tmp.write('id,name
1,Alice
')
    tmp.flush()
    print(f"Temp file: {tmp.name}")
    # Process the temp file here
# File deleted automatically here

# Temporary directory
with tempfile.TemporaryDirectory() as tmpdir:
    work_path = Path(tmpdir) / 'intermediate.bin'
    work_path.write_bytes(b'processing...')
    # All files in tmpdir deleted automatically

# Atomic writes — write to temp then rename (prevents partial writes)
def atomic_write(target_path: Path, content: str):
    tmp = target_path.with_suffix('.tmp')
    tmp.write_text(content, encoding='utf-8')
    tmp.replace(target_path)  # Atomic on POSIX; near-atomic on Windows

Reading Different File Encodings

# Read with explicit encoding (always specify for portability)
with open('data.csv', encoding='utf-8') as f:
    content = f.read()

# Handle files with BOM (Byte Order Mark)
with open('windows_export.csv', encoding='utf-8-sig') as f:
    content = f.read()  # BOM stripped automatically

# Safe multi-encoding read attempt
def read_file_smart(path):
    for enc in ['utf-8', 'utf-8-sig', 'cp1252', 'latin-1']:
        try:
            with open(path, encoding=enc) as f:
                return f.read()
        except UnicodeDecodeError:
            continue
    # Last resort: replace undecodable bytes with ?
    with open(path, encoding='utf-8', errors='replace') as f:
        return f.read()

# Common encodings
# utf-8: Default for modern web, JSON, Python source
# utf-16: Windows Unicode files (has BOM)
# latin-1: ISO-8859-1, Western European legacy
# cp1252: Windows Western European (superset of latin-1)

ConfigParser — INI Configuration Files

import configparser

# Config file (config.ini):
# [database]
# host = localhost
# port = 5432
# name = myapp

config = configparser.ConfigParser()
config.read('config.ini', encoding='utf-8')

host = config.get('database', 'host', fallback='localhost')
port = config.getint('database', 'port', fallback=5432)
debug = config.getboolean('app', 'debug', fallback=False)

# Write config
config['logging'] = {'level': 'INFO', 'file': 'app.log'}
with open('config.ini', 'w') as f:
    config.write(f)

File Walking and Directory Operations

from pathlib import Path
import shutil

project = Path('my_project')

# Walk all files recursively
for file in project.rglob('*.py'):
    size = file.stat().st_size
    modified = file.stat().st_mtime
    print(f"{file.relative_to(project)} — {size} bytes")

# Copy, move, delete with shutil
shutil.copy('source.txt', 'dest.txt')                  # Copy file
shutil.copytree('src_dir', 'dst_dir')                  # Copy entire directory
shutil.move('old_location.txt', 'new_location.txt')    # Move/rename
shutil.rmtree('temp_directory')                        # Delete directory tree

# Get disk usage
usage = shutil.disk_usage('/')
print(f"Free: {usage.free / 1e9:.1f} GB / Total: {usage.total / 1e9:.1f} GB")

Excel and Spreadsheet Files

# openpyxl — read/write .xlsx files (pip install openpyxl)
import openpyxl

# Write a spreadsheet
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sales"

# Headers
ws.append(["Date", "Product", "Quantity", "Price", "Total"])

# Data rows
data = [("2026-06-01", "Widget A", 100, 9.99, 999.0),
        ("2026-06-02", "Widget B", 50, 14.99, 749.5)]

for row in data:
    ws.append(row)

# Formatting
from openpyxl.styles import Font, PatternFill
header_row = ws[1]
for cell in header_row:
    cell.font = Font(bold=True)

wb.save("sales_report.xlsx")

# Read a spreadsheet
wb = openpyxl.load_workbook("sales_report.xlsx")
ws = wb.active

for row in ws.iter_rows(min_row=2, values_only=True):  # Skip header
    date, product, qty, price, total = row
    print(f"{product}: {qty} units @ {price:.2f} = {total:.2f}")

# xlrd — read older .xls files (pip install xlrd)
# pandas — for data analysis with DataFrames (pip install pandas)
import pandas as pd
df = pd.read_excel("sales_report.xlsx")
print(df.describe())

Environment-Aware File Paths

from pathlib import Path
import os

def get_data_dir() -> Path:
    """Return the appropriate data directory based on the environment."""
    # Check explicit override first
    if env_path := os.getenv('DATA_DIR'):
        return Path(env_path)
    
    # Production: use /var/data or similar
    if os.getenv('ENVIRONMENT') == 'production':
        return Path('/var/app/data')
    
    # Development: use local directory
    return Path(__file__).parent.parent / 'data'

DATA_DIR = get_data_dir()
DATA_DIR.mkdir(parents=True, exist_ok=True)

# Usage throughout the app
users_file = DATA_DIR / 'users.json'
cache_dir = DATA_DIR / 'cache'

Frequently asked questions: File handling

What is file handling in Python?

File handling means reading from and writing to files so data persists beyond program execution.

What is the with statement in Python file handling?

with open(...) automatically closes files and is the recommended safe pattern.

What is the difference between 'r', 'w', and 'a' modes?

'r' reads existing files, 'w' overwrites or creates, and 'a' appends while preserving content.

What is the difference between read(), readline(), and readlines()?

read() returns full content, readline() returns one line, readlines() returns a list of lines.

How do I read a CSV file in Python?

Use csv.DictReader for header-based row access and open files with newline="" for compatibility.

How do I read and write JSON files in Python?

Use json.load/json.dump for files and json.loads/json.dumps for strings.

What is pathlib and why should I use it?

pathlib provides modern, readable, cross-platform path handling with Path objects.

How do I handle file errors in Python?

Catch specific exceptions like FileNotFoundError, PermissionError, and IsADirectoryError.