1. Home
  2. Blog
  3. Software Development

Software Development

Python File Handling: Read, Write, CSV, JSON and pathlib With Examples

Almost every real script reads or writes a file. Learn the safe way to open files, choose the right mode, handle CSV and JSON, work with paths using pathlib, and avoid the mistake that wipes your data.

9 min read
Python file handling: read, write, CSV, JSON and pathlib with examples

Key takeaways

  • Always open files with a with block, so they close automatically.
  • Mode "w" erases an existing file. Use "a" to add to one, or "x" to refuse overwriting.
  • Loop over the file object to read large files without loading everything into memory.
  • Pass encoding="utf-8" explicitly, and prefer pathlib for building paths.

Logs, reports, configuration, exports: sooner or later, every script touches the file system. Python file handling is simple to start with, but a few details decide whether your code is reliable or quietly destroys data.

This guide covers text files, CSV, JSON, binary data and paths, with a focus on safe habits. We ran these examples on Python 3.13 in a temporary folder, and the output shown comes from that run.

Open files with open() and with

The built-in open() function returns a file object. Use it inside a with block:

with open("notes.txt", "w", encoding="utf-8") as file:
    file.write("first line\n")
    file.write("second line\n")

print(file.closed)

# True

When the block ends, Python closes the file, even if an error occurred inside it. Without with, you have to call close() yourself, and an exception can skip that line. Using a file after it has closed raises an error:

file.read()

# ValueError: I/O operation on closed file.

File modes

The second argument to open() says what you plan to do:

Mode Meaning If the file exists If it does not
r Read (default) Opens it Error
w Write Erases it first Creates it
a Append Adds to the end Creates it
x Exclusive create Error Creates it
r+ Read and write Opens it Error

Add b for binary data, as in "rb" or "wb". Text is the default, and it can also be written explicitly as t.

Write and append

write() does not add a line break, so include \n yourself. Appending keeps what is already there:

with open("notes.txt", "a", encoding="utf-8") as file:
    file.write("third line\n")

Compare that with mode "w", which replaced our three lines with one, with no warning at all:

with open("notes.txt", "w", encoding="utf-8") as file:
    file.write("replaced\n")

# notes.txt now contains only: replaced

When you must not overwrite something, use "x". It raised FileExistsError in our test, since notes.txt already existed.

Read a file

There are several ways to read, and the right choice depends on the file’s size:

Approach Returns Best for
file.read() One string with everything Small files
file.readline() The next line Reading a header
file.readlines() A list of lines Small files you need as a list
for line in file One line at a time Any size, especially large files
with open("notes.txt", encoding="utf-8") as file:
    print(repr(file.read()))

# 'first line\nsecond line\n'

with open("notes.txt", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

# first line
# second line
# third line

Notice the \n characters kept in the raw string. strip() removes them. A file object also remembers its position, so reading continues where it left off:

with open("notes.txt", encoding="utf-8") as file:
    print(file.readline().strip())
    print(file.readlines())

# first line
# ['second line\n', 'third line\n']

Looping over the file is the key technique for big logs and exports. It reads a line at a time, so a multi-gigabyte file does not have to fit in memory.

Handle file errors

Files may be missing, locked or unreadable, so plan for that:

try:
    with open("missing.txt", encoding="utf-8") as file:
        data = file.read()
except FileNotFoundError as error:
    print(error)

# [Errno 2] No such file or directory: 'missing.txt'
Exception Typical cause
FileNotFoundError The path does not exist
FileExistsError Mode x on an existing file
PermissionError Not allowed, or locked on Windows
IsADirectoryError The path is a folder
UnicodeDecodeError The wrong encoding was used

Try opening the file and catching the error, rather than checking first with exists(), since the file could vanish between the check and the open. Our guide to Python exception handling covers this approach.

CSV files

Do not split CSV lines by comma yourself, because quoted values can contain commas. The standard csv module handles those rules. DictWriter and DictReader are the most readable options:

import csv

with open("orders.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=["id", "customer", "total"])
    writer.writeheader()
    writer.writerow({"id": 1, "customer": "Asha", "total": 1200})
    writer.writerow({"id": 2, "customer": "Ben", "total": 300})

with open("orders.csv", newline="", encoding="utf-8") as file:
    rows = list(csv.DictReader(file))

print(rows[0])
print(sum(int(r["total"]) for r in rows))

# {'id': '1', 'customer': 'Asha', 'total': '1200'}
# 1500

Two details matter. Pass newline="" when opening CSV files, as the documentation recommends, to prevent blank rows on Windows. And every value comes back as a string, even numbers, which is why we needed int() in the sum. For heavy data work, a library such as pandas can be a better fit.

JSON files

JSON is the usual format for configuration and API data. It maps neatly onto Python dictionaries and lists:

import json

config = {"debug": True, "retries": 3, "hosts": ["a", "b"]}

with open("config.json", "w", encoding="utf-8") as file:
    json.dump(config, file, indent=2)

with open("config.json", encoding="utf-8") as file:
    print(json.load(file)["retries"])

# 3

Remember the naming: dump and load work with files, while dumps and loads work with strings. Types such as dates cannot be stored directly, so you will need to convert them to strings first.

Work with paths using pathlib

Gluing paths together with strings breaks across operating systems. The standard pathlib module handles separators for you and offers handy shortcuts:

from pathlib import Path

path = Path("data") / "reports" / "summary.txt"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("all good\n", encoding="utf-8")

print(path.read_text(encoding="utf-8").strip())   # all good
print(path.suffix, path.stem, path.exists())      # .txt summary True
print(sorted(p.name for p in Path(".").glob("*.csv")))
# ['orders.csv']
Task pathlib
Join paths Path("a") / "b.txt"
Create folders path.mkdir(parents=True, exist_ok=True)
Read or write whole text read_text(), write_text()
Find files glob("*.csv"), rglob("*.py")
Delete a file path.unlink()
Get the current script’s folder Path(__file__).parent

That last one solves a common headache. Relative paths resolve from where you ran the script, not from where the script lives, so building paths from Path(__file__).parent avoids many “file not found” surprises.

Binary files

Images, PDFs and ZIP files are not text. Open them with b, and you read and write bytes, with no encoding involved:

with open("pixel.bin", "wb") as file:
    file.write(bytes([255, 0, 128]))

with open("pixel.bin", "rb") as file:
    print(file.read())

# b'\xff\x00\x80'

Safer writes

If a program crashes halfway through writing an important file, you can be left with a corrupted result. A common fix is to write to a temporary file first, and then swap it into place:

temp = Path("settings.tmp")
temp.write_text("new", encoding="utf-8")
temp.replace("settings.txt")

print(Path("settings.txt").read_text(), temp.exists())

# new False

replace() overwrites the destination. On the same file system, the rename is generally atomic, so readers see either the old file or the new one rather than a half-written mix.

Tip: Never build file paths directly from user input without checking them. A value like ../../secrets.txt could reach files outside your intended folder. This is known as a path traversal vulnerability.

A practical example: summarize a log

This short script puts several ideas together. It reads a log one line at a time, so its size does not matter, counts each level using Counter, and writes a summary to a new file:

from collections import Counter

counts = Counter()
with open("app.log", encoding="utf-8") as file:
    for line in file:
        counts[line.split()[0]] += 1

with open("summary.txt", "w", encoding="utf-8") as out:
    for level, total in counts.most_common():
        out.write(f"{level}: {total}\n")

# summary.txt
# ERROR: 2
# INFO: 2
# WARN: 1

Our sample log had five lines. A blank line would break line.split()[0], so real scripts should skip empty lines first.

Common mistakes

  • Using "w" when you meant "a". Your existing data is gone immediately on open, even if you never write anything.
  • Omitting the encoding. The default varies by system, so a script that works on Linux can fail on Windows with accented text or emoji.
  • Calling read() on huge files. This can exhaust memory, so loop over lines instead.
  • Forgetting int() on CSV values. "10" + "20" gives "1020".
  • Leaving out newline="" for CSV. You may get unexpected blank rows on Windows.
  • Not testing file code. Use pytest’s tmp_path fixture so that tests never touch real files. See our pytest tutorial.

Final thoughts

A few habits cover almost everything here: use with, set the encoding, think twice about "w", and let csv, json and pathlib do the hard parts. To practise, write a small script that reads a CSV, totals one column and saves a JSON summary to a new folder.

Frequently asked questions

How do I read a file line by line in Python?

Open it in a with block and loop over the file object. This reads one line at a time, so it works well even for very large files.

What is the difference between w and a mode?

Mode w erases any existing content as soon as the file opens. Mode a keeps existing content and adds new data to the end of the file.

Why should I use with to open a file?

The with statement closes the file automatically when the block ends, even if an error occurs. This prevents leaks and helps ensure data is written.

How do I check if a file exists in Python?

Use Path(“name.txt”).exists() from pathlib. If you plan to open it straight after, catching FileNotFoundError is usually more reliable than checking first.

Why does my CSV have blank lines on Windows?

You probably opened the file without newline=””. The csv module manages line endings itself, so pass that argument to avoid extra blank rows.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Software Development

Python Dataclasses Explained: Less Boilerplate, Safer Defaults and Real Examples

Python dataclasses explained with code we ran: generated methods, default_factory, frozen and ordered classes, __post_init__, slots, kw_only and when to…

Read article →
Software Development

JavaScript Destructuring Explained: Objects, Arrays, Defaults and Common Errors

JavaScript destructuring explained with code we ran in Node.js: objects, arrays, renaming, defaults, nested patterns, rest, function parameters and the…

Read article →
Software Development

Python f-strings Explained: Formatting Numbers, Dates and Debug Output

Python f-strings explained with code we ran: format specifiers for numbers, alignment and dates, the = debug shortcut, rounding surprises,…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
DevOps

Environment Variables Explained: .env Files, Secrets and Config Done Right

Environment variables explained with Python examples we ran: reading and setting them, why every value is a string, .env files,…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Databases & SQL

DELETE vs TRUNCATE vs DROP in SQL: Differences, Rollback and When to Use Each

DELETE vs TRUNCATE vs DROP explained with tested SQL: what each removes, rollback, triggers, identity reset, speed, and how PostgreSQL,…

Read article →
Databases & SQL

SQL Views Explained: Create, Use and Avoid the Pitfalls, With Examples

SQL views explained with queries we ran: creating a view, live results, hiding columns, performance, updatable and materialized views, broken…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
DevOps

Monitoring vs Observability: The Real Difference, With Logs, Metrics and Traces

Monitoring vs observability explained: known and unknown failures, logs, metrics and traces, a checkout incident walkthrough, golden signals, OpenTelemetry and…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →
Software Testing

Test Case vs Test Scenario: Key Differences, Examples and When to Use Each

Test case vs test scenario explained: clear definitions, a side-by-side table, one password reset feature taken from scenarios to cases,…

Read article →
Artificial Intelligence

What Is a Large Language Model? How LLMs Work, Explained Without the Hype

What is a large language model? LLMs explained in plain language: tokens, embeddings, attention, training, temperature and context windows, a…

Read article →
Test Automation

Selenium vs Playwright: Key Differences, Code Compared and Which to Choose

Selenium vs Playwright compared: architecture, waiting, browser and language support, debugging, the same test in both tools, migration advice and…

Read article →