json.dumps(list(csv.DictReader(f)), indent=2) turns an open CSV file into a JSON array with one object per row, keyed by the header row. csv.DictReader does the CSV parsing, quoted commas and doubled quotes included, so there’s no reason to split lines on , yourself.
import csv, json
with open("people.csv", newline="", encoding="utf-8-sig") as f:
rows = list(csv.DictReader(f))
print(json.dumps(rows, indent=2, ensure_ascii=False))
For this people.csv:
id,name,city,zip
1,"Lovelace, Ada",London,02134
2,Kurt Gödel,Wien,1090
3,"Alan ""Al"" Turing",,
it prints (Python 3.14):
[
{
"id": "1",
"name": "Lovelace, Ada",
"city": "London",
"zip": "02134"
},
{
"id": "2",
"name": "Kurt Gödel",
"city": "Wien",
"zip": "1090"
},
{
"id": "3",
"name": "Alan \"Al\" Turing",
"city": "",
"zip": ""
}
]
The converter below prints exactly the same, with its typing box unticked.
Everything stays a string
DictReader never guesses types: id is "1", and an empty cell is "", not None. That’s the safe default for a column like zip, where 02134 as a number would lose its zero. Convert the columns you know:
rows = [{**row, "id": int(row["id"])} for row in rows]
# [{'id': 1, 'name': 'Lovelace, Ada', 'city': 'London', 'zip': '02134'}, ...]
Tick Read 123, 1.5, true, false and empty cells as numbers, booleans and null in the tool and it converts only the cells that write back the way they’re spelled: 1 and 1090 become numbers, 02134 stays a string, and the empty cells become null.
The three arguments that matter
newline=""leaves line endings to the csv module, as its documentation asks for every file you hand it. Without it, Python’s newline translation turns a\r\ninside a quoted field into\nbefore the parser sees it, so a multi-line cell from Excel comes back changed.encoding="utf-8-sig"drops the byte order mark that Excel’s “CSV UTF-8” format writes first. With plainutf-8it stays in the first column name,{"id": "1", ...}, androw["id"]raisesKeyError. With no encoding at all,openuses the locale’s, which on Windows is usually not UTF-8.ensure_ascii=FalsewritesGödelas it is. The default escapes every non-ASCII character,"Kurt Gödel": still valid JSON, but harder to read.
Ragged rows
A row with more fields than the header puts the extras in a list under the key None, and json.dumps writes that key as "null". A short row’s missing fields are None. For id,name, then 1,Ada,extra, then 2:
[{"id": "1", "name": "Ada", "null": ["extra"]}, {"id": "2", "name": null}]
restkey="extra" names the overflow, and json.dumps(..., sort_keys=True) fails with a TypeError while a None key is there. The tool calls the extra column column3, gives the short row null for the fields it lacks, as DictReader does, and warns about both rows.
Big files and the shell
For a file you don’t want in memory at once, write JSON Lines, one object per line:
with open("people.csv", newline="", encoding="utf-8-sig") as f, open("people.jsonl", "w", encoding="utf-8") as out:
for row in csv.DictReader(f):
out.write(json.dumps(row, ensure_ascii=False) + "\n")
From a terminal, the same conversion is one line:
python3 -c 'import csv,json,sys; print(json.dumps(list(csv.DictReader(open(sys.argv[1], newline="", encoding="utf-8-sig"))), indent=2, ensure_ascii=False))' people.csv
For a semicolon file, pass delimiter=";" to DictReader.