csv to json
CSV to JSON in Python, with real output
Two ways to convert CSV to JSON in Python: the csv and json modules that ship with the standard library, and pandas. Both run below against the same live file, the 22-row HUD fair market rent sample, and each output block is what the command actually printed on 2026-09-10. Along the way: quoted commas that break naive splitting, the two output shapes and when each one is right, and the JSON shape our own dataset files use that breaks scripts written for the other shape.
Get the sample file
The HUD rent sample is a static file on this site, no key needed. It returned HTTP 200 on 2026-09-10.
curl -s -o hud-sample.csv https://jayjex.github.io/data-vault/data/hud-fmr-2026/sample.csv head -3 hud-sample.csv
zip,hud_area_code,metro,area_name,state,fmr_0br,fmr_1br,fmr_2br,fmr_3br,fmr_4br 76437,METRO10180M10180,metro,"Abilene, TX MSA",TX,850,880,1090,1420,1710 76443,METRO10180M10180,metro,"Abilene, TX MSA",TX,850,860,1090,1420,1710
One header row, 22 data rows, 10 columns. Look at "Abilene, TX MSA": the field holds a comma, so the file quotes it. A naive line.split(",") cuts that field in two and shifts every column after it. The csv module and pandas both handle the quoting, which is most of the reason to never parse CSV by hand.
Standard library, shape one: array of arrays
csv.reader treats every row as a list of strings, header included. Numbers stay strings: the rent values arrive as "850", not 850.
import csv, json
with open("hud-sample.csv", newline="") as f:
rows = list(csv.reader(f))
print(json.dumps(rows[:2]))
[["zip", "hud_area_code", "metro", "area_name", "state", "fmr_0br", "fmr_1br", "fmr_2br", "fmr_3br", "fmr_4br"], ["76437", "METRO10180M10180", "metro", "Abilene, TX MSA", "TX", "850", "880", "1090", "1420", "1710"]]
The first element is the header, so a consumer reads rows[0] for column names and rows[1:] for data, all position-based. Field names appear once instead of in every record, which makes this the smaller of the two shapes on disk.
Standard library, shape two: array of objects
csv.DictReader uses the header row as keys and yields one dict per row. This is the shape most APIs and document stores want.
import csv, json
with open("hud-sample.csv", newline="") as f:
records = list(csv.DictReader(f))
print(len(records))
print(json.dumps(records[0]))
22
{"zip": "76437", "hud_area_code": "METRO10180M10180", "metro": "metro", "area_name": "Abilene, TX MSA", "state": "TX", "fmr_0br": "850", "fmr_1br": "880", "fmr_2br": "1090", "fmr_3br": "1420", "fmr_4br": "1710"}
Every value is still a string. If downstream code needs numbers, cast before dumping, for example int(r["fmr_2br"]) per record, or let pandas do the inference in the next section.
With pandas: dtype inference and to_json
pd.read_csv infers types, and to_json with orient="records" writes the array of objects with numbers unquoted.
import pandas as pd
df = pd.read_csv("hud-sample.csv")
print({k: str(v) for k, v in df.dtypes.items()})
df.head(2).to_json("hud-sample.json", orient="records", indent=2)
{'zip': 'int64', 'hud_area_code': 'str', 'metro': 'str', 'area_name': 'str', 'state': 'str', 'fmr_0br': 'int64', 'fmr_1br': 'int64', 'fmr_2br': 'int64', 'fmr_3br': 'int64', 'fmr_4br': 'int64'}
[
{
"zip":76437,
"hud_area_code":"METRO10180M10180",
"metro":"metro",
"area_name":"Abilene, TX MSA",
"state":"TX",
"fmr_0br":850,
"fmr_1br":880,
"fmr_2br":1090,
"fmr_3br":1420,
"fmr_4br":1710
},
{
"zip":76443,
"hud_area_code":"METRO10180M10180",
"metro":"metro",
"area_name":"Abilene, TX MSA",
"state":"TX",
"fmr_0br":850,
"fmr_1br":860,
"fmr_2br":1090,
"fmr_3br":1420,
"fmr_4br":1710
}
]
Inference is a trade. Rent columns arriving as int64 is what you want. But zip also became int64, and any ZIP starting with zero, all of New England, would turn 02138 into the integer 2138. Pass dtype={"zip": "string"} to read_csv when codes must keep leading zeros. One more trap: with NaN values in float columns, Python's json.dumps writes bare NaN, which strict JSON parsers reject; to_json emits null instead.
Which shape to pick, and a shape gotcha from this site
Array of objects for almost everything. Each record names its own fields, consumers need no header logic, and reordering or adding columns cannot silently shift values. Array of arrays suits position-based formats and saves bytes when a file has many rows and you control both ends.
Now the gotcha, from our own files. Every dataset JSON on this site, like the HUD sample, follows one envelope: {slug, name, columns[], row_count, records[]}, where records is the array of objects. A script written for an array of arrays starts with d[0] and dies on the first line, because d is an object with named keys. The fix is one step: read d["records"] for the rows and d["columns"] for the schema. When you publish JSON yourself, state the shape at the top of the docs. A consumer who assumes the other shape gets a broken script, not an error message.
FAQ
How do I convert a CSV file to JSON in Python?
With the standard library alone: open the file, pass it to csv.DictReader, wrap the reader in list(), then json.dump the result to a file or json.dumps it to a string. That produces an array of objects keyed by the header row. If you want an array of arrays instead, use csv.reader. pandas does the same job in two lines with pd.read_csv followed by to_json(orient="records").
Should CSV to JSON output be an array of arrays or an array of objects?
Array of objects for almost everything: each record names its own fields, consumers need no header logic, and adding or reordering columns cannot silently shift values. Array of arrays fits position-based formats and produces smaller files because field names appear once in the header instead of in every record. Whichever you pick, state the shape in your API or file docs, because a consumer who assumes the other shape gets a broken script, not an error message.
How do I keep numbers as numbers when converting CSV to JSON?
The csv module reads every value as a string, so 850 arrives as "850"; cast the columns you need to int or float before dumping. pandas infers dtypes for you and its to_json writes numbers unquoted, but watch a real cost: a ZIP code like 02138 becomes the integer 2138. Keep ZIP and code columns as dtype="string" in read_csv when leading zeros matter.
Where to go next
The sample used here, plus four more in CSV and JSON, are listed with row counts and column lists on sample data downloads. Deciding between a file and a live query layer for your pipeline is the job of CSV vs API for data. If you are documenting columns before converting them, what is a data dictionary shows the five fields that matter with real entries from this same HUD file.