Python CSV to JSON: convert rows with DictReader and json.dumps
Turn CSV rows into JSON objects with headers, UTF-8 text, and explicit handling for malformed rows.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Python CSV to JSON: convert rows with DictReader and json.dumps
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
We need to convert a small CSV file into a JSON array of records. The CSV has a header row, a name containing a comma, and a quantity with leading zeroes. A simple split on commas would damage the name, while turning the parsed list into str would produce Python syntax rather than JSON. The reliable path is csv.DictReader for the records and json.dumps for the JSON text. We will keep cell values as strings; a generic converter cannot know whether 003 is a number, a code, or an identifier that must keep its leading zeroes. We will also catch rows with too many or too few cells before they become a
- 00:42
surprising JSON shape. The browser lesson uses StringIO, which is enough to exercise parsing and serialization without file access. The page also has a local file example using UTF-8 and newline handling. Predict the output shape before each run: a list of dictionaries, with keys from the first row and values from each later row. The parser did its part. It recognized the quoted comma, so the name is one field, and it kept quantity as the string '003'. The failure is in the second step: str(rows) creates Python's representation of a list of dictionaries.
- 01:23
It uses single quotes here, which are not JSON string delimiters. Some Python values also have spellings such as None or True that do not match JSON's null or true. A returned string can look convincing in a log and still be invalid for a JSON consumer. Avoid constructing JSON by string concatenation for the same reason. Let json.dumps apply the format's escaping and value rules. Before fixing the serializer, inspect one more issue in the parsed structure. DictReader is permissive about uneven rows, and we should decide whether those rows belong in our output.
- 02:03
Our example contract expects exactly two columns, name and quantity. We will reject records that do not match that shape. DictReader does not raise automatically when a row is uneven. With an extra cell, it stores the surplus under its restkey, None by default. With a missing cell, it supplies None as the value. In contrast, a trailing comma means a cell was present but empty, so its value is an empty string. Those differences matter. If we serialize the extra-cell row without checking, JSON encoding of a dictionary with a None key can produce a key that the original CSV never named.
- 02:44
If we serialize a missing cell, it can become JSON null, which may be mistaken for a deliberate value. Our converter rejects extra and missing cells but allows an explicitly empty string. A later domain rule could reject empty quantity if the application needs that; structural conversion should not silently invent such a rule. We also verify the header row before reading data. If a file changes from name to person, silently emitting a different JSON key would surprise consumers of the output. The corrected function checks the exact header for this teaching example, then checks every row for surplus or missing cells.
- 03:26
Only then does json.dumps serialize the list of dictionaries. The output uses JSON double quotes, and json.loads can parse it back. The leading zeroes remain because we did not guess that quantity should be an integer. This is the default behavior of CSV parsing: cells arrive as text. If your real schema says quantity is numeric, validate and convert that specific column before serialization, and decide how to report an invalid value. Do not convert every digit-looking string, since postal codes, product IDs, and account numbers may need their original form. ensure_ascii=False keeps non-ASCII
- 04:06
text readable. When writing to a file, pair that with an explicit UTF-8 encoding. A pretty indent is optional and changes layout only. This example's schema is intentionally narrow, so a CSV with other legitimate columns should get its own stated contract. The successful case proves that CSV quoting and UTF-8 text survive the conversion, and the JSON round trip proves the output is parseable. The two rejected cases show the example's structural policy in action. A third test with a trailing comma would pass the structural check and keep an
- 04:47
empty quantity string, because empty is not the same as absent. If your domain treats empty quantity as invalid, add that rule separately. For a real CSV file, open it with newline='' and encoding='utf-8', and write the JSON output with encoding='utf-8'. DictReader will then handle quoted record boundaries correctly across platforms. For a huge input, building a complete JSON array in memory may be too expensive; JSON Lines or streaming is a separate design choice. The browser task deliberately uses a small string and asks you to implement this exact conversion.
- 05:29
Check the parsed JSON structure rather than only comparing the printed text, because indentation and whitespace can vary without changing the data.
Understand the concept
Python CSV to JSON conversion has two separate steps: parse CSV into records, then serialize those records as JSON. csv.DictReader uses the first row as field names and produces one dictionary for each later row. json.dumps turns a list of those dictionaries into JSON text. A CSV value such as 3 remains the string '3' unless you deliberately parse it. This guide preserves text values because a generic converter cannot know which columns represent integers, dates, postal codes, or identifiers with leading zeroes.
For a real file, open the CSV with encoding='utf-8' and newline='' as recommended by the csv module. Open the JSON output with encoding='utf-8' and write json.dump(rows, output, ensure_ascii=False, indent=2). The browser example uses io.StringIO so it is fully reproducible without a local file. The same parse-and-serialize steps apply in both places. CSV quoting matters: a field such as 'Ada, Jr.' must remain one value even though it contains a comma.
A conversion should inspect headers before trusting records. DictReader can place surplus columns under a None key, and fill missing columns with None. If those cases are silently serialized, the resulting JSON shape may be surprising. This lesson requires exactly name and quantity headers, then rejects any row whose keys or values show extra or missing cells. That is a small, stated contract for the example, not a claim that every CSV file has these two columns.
After conversion, round-trip the text with json.loads to confirm it is valid JSON and has the expected structure. Do not compare JSON text by eye alone, since indentation and key order are presentation details. If your CSV uses a different delimiter, pass it explicitly to DictReader. If your data includes untrusted spreadsheet formulas, consider spreadsheet-export handling separately; JSON serialization by itself does not sanitize future CSV exports.
- DictReader maps header names to text values
- json.dumps serializes Python rows
- CSV quoting preserves embedded commas
- Validate ragged rows before export
See it step by step
Read the code, predict the output, then compare it with the result.
01. Parse quoted CSV fields
import csv
from io import StringIO
source = 'name,quantity\n"Ada, Jr.",3\n'
print(list(csv.DictReader(StringIO(source))))[{'name': 'Ada, Jr.', 'quantity': '3'}]DictReader treats the comma inside quotes as part of the name and preserves quantity as text.
02. Serialize rows to JSON
import json
rows = [{'name': 'café', 'quantity': '003'}]
text = json.dumps(rows, ensure_ascii=False)
print(text)
print(json.loads(text)[0]['quantity'])[{"name": "café", "quantity": "003"}]
003ensure_ascii=False keeps readable UTF-8 characters; json.loads checks the structure and leading zeroes.
03. Detect extra and missing cells
import csv
from io import StringIO
for source in ('name,quantity\nAda,3,extra\n', 'name,quantity\nAda\n'):
row = next(csv.DictReader(StringIO(source)))
print(None in row, any(value is None for value in row.values()))True False
False TrueDictReader exposes ragged data; the converter must decide whether to reject it.
Convert a real CSV file to JSON
This self-contained example creates sample CSV and JSON files in a temporary directory. Replace those paths with your own files after verifying the conversion.
import csv
import json
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
source = Path(folder) / 'items.csv'
target = Path(folder) / 'items.json'
source.write_text('name,quantity\ncafé,003\n', encoding='utf-8')
with source.open('r', encoding='utf-8', newline='') as handle:
reader = csv.DictReader(handle)
if reader.fieldnames != ['name', 'quantity']:
raise ValueError('expected name,quantity header')
rows = list(reader)
if any(None in row or any(value is None for value in row.values()) for row in rows):
raise ValueError('CSV has missing or extra cells')
with target.open('w', encoding='utf-8') as handle:
json.dump(rows, handle, ensure_ascii=False, indent=2)
print(target.read_text(encoding='utf-8'))A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Parse CSV rows into dictionaries
A CSV file is not safely parsed by splitting every line on commas. Quoted fields may contain commas or newlines. csv.DictReader applies CSV quoting rules and uses the header row as dictionary keys. The first example contains 'Ada, Jr.' as one name value. Its quantity is still a string, because CSV does not carry a built-in integer type. This is a useful property when values such as 003 must keep leading zeroes.
For an actual file, open with encoding='utf-8' and newline=''. The csv module handles newline conventions itself, so that argument matters across operating systems. In the browser, StringIO provides the same file-like interface for a small fixture. Both paths let you inspect reader.fieldnames before consuming all rows. If the file's schema differs from the expected name,quantity columns, fail with a clear error instead of exporting mislabeled JSON.
import csv
from io import StringIO
source = 'name,quantity\n"Ada, Jr.",3\n'
reader = csv.DictReader(StringIO(source))
print(reader.fieldnames)
print(next(reader))['name', 'quantity']
{'name': 'Ada, Jr.', 'quantity': '3'}- Use DictReader rather than manual string splitting.
- Inspect fieldnames and the first parsed row.
Reject rows that do not match the header
DictReader is flexible by design. An extra cell can be collected under the restkey, which defaults to None. A missing cell can be filled with None. Those outputs are not automatically suitable for a two-column JSON shape. In this lesson, a row with either case raises ValueError. An empty file is also rejected because it has no expected header. A different application may choose a recovery rule, but the choice should be explicit.
Do not mistake a blank text value for a missing cell. The CSV line Ada, contains a present but empty quantity, whereas Ada alone lacks the second cell. This example's structural validation allows the empty string and leaves any business rule about required quantity for a separate step. That keeps parsing, structural checks, and domain checks understandable.
import csv
from io import StringIO
for source in ('name,quantity\nAda,3,extra\n', 'name,quantity\nAda\n', 'name,quantity\nAda,\n'):
row = next(csv.DictReader(StringIO(source)))
print(None in row, any(value is None for value in row.values()))True False
False True
False False- Reject a None key created by surplus cells.
- Reject None values created by missing cells.
Serialize and verify the JSON
After validating the records, pass the list of dictionaries to json.dumps. The output is JSON text, unlike str(rows), which is Python's representation of a list. ensure_ascii=False keeps Unicode readable when the result is written with UTF-8, and indent=2 makes a file easier to inspect. Neither option changes the parsed values. If the target system expects numbers, convert specified columns deliberately before serializing; never infer all digit-looking text is numeric.
Use json.loads on the output in a test to verify its structure. Round-tripping catches malformed JSON and lets you assert field values without depending on whitespace formatting. For large files, building every row in memory may be expensive. Consider streaming or JSON Lines for that use case; this lesson deliberately teaches a small, conventional JSON array conversion.
import json
rows = [{'name':'café','quantity':'003'}]
text = json.dumps(rows, ensure_ascii=False)
print(text)
print(json.loads(text)[0]['quantity'])[{"name": "café", "quantity": "003"}]
003- Serialize a list of DictReader records with json.dumps.
- Parse the result again and check a value with leading zeroes.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use csv.DictReader(StringIO(csv_text)), check reader.fieldnames, reject rows containing a None key or None value, and return json.dumps(rows, ensure_ascii=False).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Serializing Python repr instead of JSON
return str(list(csv.DictReader(stream)))Pass the list of row dictionaries to json.dumps.What happens: The text uses Python quotes and is not guaranteed valid JSON.
JSON has its own serialization rules and escaping.
Silently accepting a ragged row
rows = list(csv.DictReader(stream)); json.dumps(rows)Check headers and each row before serializing.What happens: Extra cells may appear under a None key; missing cells may become null.
A predictable output schema depends on validating the CSV structure.