JSON in Python: Reading and Writing Files, CSV, pathlib and UTF-8 Encoding
Python has a built-in json module for working with JSON: json.load reads data from an open file, json.dump writes it to a file, and json.loads and json.dumps do the same with strings. Open files with with open(path, encoding="utf-8"), build paths conveniently with pathlib.Path, and read tables with the csv module.
Working with JSON in Python requires no third-party libraries: the json module is part of the standard library and turns dictionaries and lists into text and back. Almost always you also need three more things: the open function with the with construct, the pathlib module for paths and the csv module for tables. Below is everything in order, with working code and the pitfalls people most often stumble on.
The open function in Python: modes and encoding
Any text file is opened with the built-in function open(path, mode, encoding=...). It returns a file object that has the read, readline and write methods and that you can iterate over line by line in a loop.
| Mode | What it does |
|---|---|
"r" | Reading (the default). If the file does not exist — FileNotFoundError |
"w" | Writing. The file is created or emptied immediately on opening |
"a" | Appending to the end; existing content is left untouched |
"x" | Create a new file; if it already exists — FileExistsError |
"b" | A modifier for the mode: working with bytes ("rb", "wb") |
Why you need with
A file must be closed: otherwise the data may not be written to disk, and the system keeps the handle busy. The with construct closes the file automatically — even if an error happens inside the block.
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.write("Second line\n")
with open("notes.txt", encoding="utf-8") as f:
for line in f:
print(line.rstrip("\n"))
Iterating over a file in a loop reads it one line at a time without loading it entirely into memory, so you can process even gigabyte-sized logs this way. The f.read() method returns the whole content as one string, and f.readlines() returns a list of lines.
How to write a line to a file without erasing the old content
The most common beginner mistake is opening a file in "w" mode to "add" a line. The "w" mode empties the file at the moment it is opened, before the first write. To append a record to the end, use "a", and to make sure you never overwrite someone else's file, use "x".
with open("log.txt", "a", encoding="utf-8") as f:
f.write("new record\n")
Note that write does not add a line break by itself — "\n" must be written explicitly. You can also use print(..., file=f), which adds the line break for you.
UTF-8 encoding: why "mojibake" appears
Before Python 3.15, the open() function without the encoding parameter used the system encoding. On Linux and macOS that is UTF-8, while on Windows with a Russian locale it is cp1251. A script that worked fine on a server would print "Еда" instead of "Еда" on a colleague's laptop, or crash with a UnicodeDecodeError.
In Python 3.15, under PEP 686, UTF-8 mode is enabled by default: open() without encoding reads and writes UTF-8 on all operating systems. You can restore the old behaviour with the environment variable PYTHONUTF8=0 or the flag -X utf8=0, and explicitly request the system encoding with encoding="locale".
import sys
print(sys.flags.utf8_mode) # 1 in Python 3.15
open("legacy.txt", encoding="locale") # system encoding
open("old_excel.csv", encoding="cp1251") # a file saved in cp1251
The flip side: an old CSV from Excel or a Windows script saved in cp1251 will now be read as UTF-8 and cause a UnicodeDecodeError. The fix is to read it with encoding="cp1251" and save it again as UTF-8. The good practice stays the same — always specify encoding explicitly, and the code then behaves identically on any version.
pathlib in Python: paths as objects
Joining paths as strings ("data" + "/" + name) is awkward and breaks on different operating systems. The pathlib module provides the Path class, where paths are joined with the / operator and the object has clearly named properties.
from pathlib import Path
data = Path("data")
csv_path = data / "expenses.csv"
print(csv_path.name) # expenses.csv
print(csv_path.stem) # expenses
print(csv_path.suffix) # .csv
print(csv_path.parent) # data
print(csv_path.exists())
data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("Hello", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))
for p in data.glob("*.csv"):
print(p)
The read_text and write_text methods replace the open + with pair for small files. A Path object can be passed to the ordinary open, to json and to csv — all of them accept it. To get a path relative to the script itself rather than the current folder, use Path(__file__).parent / "data".
In Python 3.14, Path gained the copy() and move() methods, so shutil is no longer needed for simple copying. In the same version the standard library added the compression.zstd module for Zstandard compression (PEP 784).
JSON in Python: load, dump, loads, dumps
JSON is a text format for exchanging data: APIs return it, and configs and reports are stored in it. The json module can do two operations: serialization (a Python object → JSON text) and deserialization (text → object). There are four functions, and confusion between them is a frequent question.
| Function | What it does |
|---|---|
json.dumps(obj) | Object → JSON string |
json.loads(s) | JSON string → object |
json.dump(obj, f) | Object → write to an open file |
json.load(f) | Read JSON from an open file |
An easy way to remember: the letter s at the end stands for "string" — working with a string; without it — working with a file.
Type mapping
| Python | JSON |
|---|---|
dict | object {} |
list, tuple | array [] |
str | string |
int, float | number |
True / False | true / false |
None | null |
When read back, a tuple becomes a list, while the module does not know sets, dates and Decimal and raises TypeError: Object of type ... is not JSON serializable.
How to save JSON to a file
import json
from pathlib import Path
report = {
"month": "2026-10",
"total": 15230.5,
"categories": {"food": 8200, "transport": 2100},
"tags": ["home", "work"],
}
path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
The ensure_ascii=False parameter keeps non-ASCII characters as they are. Without it, "Еда" in the file would appear as "Еда" — valid JSON, but impossible for a human to read. indent=2 produces nicely indented formatting.
How to read JSON from a file
with open("data/report.json", encoding="utf-8") as f:
data = json.load(f)
print(data["total"]) # 15230.5
print(data["categories"]["food"]) # 8200
The result is an ordinary dictionary or list, and you work with it using the usual tools. If the file is not valid JSON or is truncated, a json.JSONDecodeError exception is raised with the line and position — it is worth catching when the data comes from outside.
Parsing a JSON string and pretty printing
import json
raw = '{"name": "Anna", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0]) # python
print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
sort_keys=True sorts the keys on output — handy for comparing files in git. To quickly format a file from the terminal there is a built-in utility: python -m json.tool data/report.json.
The response of an HTTP API is also a JSON string. The requests library parses it with the response.json() method, and underneath it is the same json.loads.
A catch: JSON keys are always strings
d = {1: "January", 2: "February"}
back = json.loads(json.dumps(d))
print(back == d) # False
print(back) # {'1': 'January', '2': 'February'}
# back[1] -> KeyError, you need back["1"]
fixed = {int(k): v for k, v in back.items()}
In JSON, object keys can only be strings, so numeric keys turn into strings after a serialization round trip. Convert them back when reading.
Custom types: dates and Decimal
import json
from datetime import date
def default(obj):
if isinstance(obj, date):
return obj.isoformat()
raise TypeError(f"cannot serialize {type(obj).__name__}")
print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
The function passed to the default parameter is called for each unknown object. For more complex cases you can subclass json.JSONEncoder and override its default method.
How to append an item to a JSON array
Unlike a text log, JSON cannot simply be appended to at the end: the file has to be read, changed and written back in full.
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "new"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
If there are many records and they are added constantly, the JSON Lines format is more convenient: one JSON object per line, and the file is opened in "a" mode.
CSV in Python: the csv module
CSV is a table in a text file: rows are records, and values are separated by commas. The csv module handles quotes, commas inside values and line breaks correctly, which a manual split(",") does not.
import csv
with open("data/expenses.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
total = 0.0
for row in reader:
total += float(row["amount"])
print(row["date"], row["category"], row["amount"])
print("Total:", total)
csv.DictReader turns each row into a dictionary keyed by the file's header. All values are strings: convert numbers and dates yourself (float, int, date.fromisoformat). A file with a ";" delimiter (as Excel saves it in many European locales) is read with the parameter delimiter=";".
rows = [
{"date": "2026-10-01", "category": "food", "amount": 450},
{"date": "2026-10-02", "category": "transport", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
writer.writeheader()
writer.writerows(rows)
When writing, newline="" is essential: without it, empty lines appear between rows on Windows. If the file will be opened in Excel, use encoding="utf-8-sig" — the BOM marker helps Excel recognise UTF-8.
From CSV to JSON: a complete example
import csv
import json
from collections import defaultdict
from pathlib import Path
src = Path("data/expenses.csv")
dst = Path("data/report.json")
by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
by_category[row["category"]] += float(row["amount"])
report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
This is the core of any report script: CSV input, aggregation in a dictionary, JSON output.
Common mistakes
FileNotFoundError— the path is relative and is resolved from the folder the script is launched from, not from the script itself. UsePath(__file__).parent.- The file was wiped — it was opened in
"w"instead of"a". UnicodeDecodeError— the file is in another encoding; specify it explicitly (cp1251,utf-8-sig).TypeError: Object of type set is not JSON serializable— convert it to a list or pass adefaultfunction.KeyErrorafter loading JSON — numeric keys have become strings.- Your own file is named
json.pyorcsv.py— it shadows the standard module, andimport jsonimports it instead.
Summary
For JSON in Python the standard library is enough: json.load/json.dump for files, loads/dumps for strings, ensure_ascii=False and indent for readable output. Open files with with and an explicit encoding, build paths with pathlib, and read tables with csv.DictReader. In our Python course this topic is practised hands-on: the expense tracker reads expenses from a CSV file and saves a report as JSON.
FAQ
How is json.load different from json.loads?
json.load reads JSON from an open file object, while json.loads parses a string. Similarly, json.dump writes to a file, and json.dumps returns a string.
How do I keep non-ASCII characters in JSON without \u sequences?
Pass ensure_ascii=False to json.dump or json.dumps and open the file with encoding="utf-8".
Do I need to install a library to work with JSON in Python?
No, the json module is built into the standard library; you just write import json.
Can I store a date or a set in JSON?
Not directly: you have to convert them — for example a date to an ISO string with isoformat, and a set to a list — or pass a function to the default parameter.
How do I read a large JSON file that does not fit in memory?
The standard json.load reads the whole file; for large volumes store the data in JSON Lines format and parse it line by line with json.loads.
This topic is covered in depth with practice in the course “Python 3.15: от новичка до профи”.
Go to course