Python JSONDecodeError, decoded
Python's json module raises exactly one exception —json.decoder.JSONDecodeError — but with several different messages, and each message points at a genuinely different mistake. This guide covers all of them, starting with the one everybody hits.
“Expecting value: line 1 column 1 (char 0)”
The most common Python JSON error by a wide margin. It means the parser looked at thefirst character and found something that cannot begin a JSON value. Since it failed at char 0, the content was never JSON — do not go hunting for a misplaced comma.
Four things cause it:
The input is empty
An empty string is not valid JSON. A 204 No Content response, an empty file, or a failed read all produce this. Check the length before parsing.
The response is HTML, not JSON
Same root cause as the JavaScript “Unexpected token <” error: a 404 page, a 500 page, or a login redirect. Print the response before parsing it:
import requests
r = requests.get(url)
print(r.status_code, r.headers.get("content-type"))
print(r.text[:300]) # look at what actually arrived
r.raise_for_status() # turn HTTP errors into exceptions
data = r.json()raise_for_status() is the single most valuable line here. It converts a silent HTML error page into a clear HTTPError before the JSON parser ever sees it.
You passed a filename instead of file contents
A classic mix-up between the two functions. json.loads parses a string;json.load reads a file object:
# ✗ Tries to parse the literal text "data.json"
data = json.loads("data.json")
# ✓ Read the file, then parse
with open("data.json", encoding="utf-8") as f:
data = json.load(f)The value is None
Passing None raises a TypeError, but the string"None" — which is what you get from an f-string or a logged value — fails at char 0 as a decode error.
“Extra data: line 2 column 1 (char 42)”
Parsing succeeded, and then found more content afterwards. A JSON document contains exactly one top-level value. This error means your file holds several, usually one object per line:
{"id": 1, "name": "Ana"}
{"id": 2, "name": "Bo"}That format is NDJSON (newline-delimited JSON), and it needs a loop, not a single parse:
records = []
with open("events.ndjson", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line: # skip blank lines
records.append(json.loads(line))The other cause is two files concatenated by accident, or a log that appended a second document to the end of the first.
“Expecting property name enclosed in double quotes”
The parser wanted a key and found something that is not a double-quoted string. Two mistakes produce it, and both come from pasting Python or JavaScript source into a JSON file:
# ✗ Single quotes and bare keys are Python/JS, not JSON
{'name': 'Ana', age: 30}
# ✗ A trailing comma leaves the parser expecting another key
{"name": "Ana", "age": 30,}
# ✓ Valid JSON
{"name": "Ana", "age": 30}If you are trying to parse the repr of a Python dict, note thatTrue, False, and None are also invalid — JSON spells them true, false, and null. Usejson.dumps to produce the text rather than str(my_dict).
“Expecting ',' delimiter” and “Expecting ':' delimiter”
Structural punctuation is missing or misplaced. The reported column is where the parser gave up, which is usually just after the real mistake — check the end of the preceding line first. A missing comma between two objects in an array is the usual culprit.
“Invalid control character at”
A raw control character — most often a literal newline or tab — appears inside a string. JSON strings may not contain unescaped characters below U+0020; they must be written\n and \t:
# ✗ Real newline inside the string
{"note": "line one
line two"}
# ✓ Escaped
{"note": "line one\nline two"}Python offers an escape hatch — json.loads(text, strict=False) permits control characters inside strings. Useful for rescuing data you did not produce, but fix the source if you can.
The BOM trap: “Expecting value” on a file that looks perfect
This one wastes hours. A file saved by Notepad, Excel, or many Windows tools as “UTF-8” begins with an invisible byte-order mark (EF BB BF). Your editor hides it. The JSON parser does not, and fails at char 0 on a file that looks flawless.
Confirm it, then fix it by choosing the right codec:
# Detect: does the file start with a BOM?
with open("data.json", "rb") as f:
print(f.read(3) == b"\xef\xbb\xbf")
# Fix: utf-8-sig strips the BOM if present, and is safe if it is absent
with open("data.json", encoding="utf-8-sig") as f:
data = json.load(f)Make utf-8-sig your default for files that arrive from other people. It costs nothing when there is no BOM. The same invisible bytes break XML parsers too — see“Content is not allowed in prolog”.
Getting the position into context
The exception carries .lineno, .colno, .pos, and.doc, so you can print the offending region instead of squinting at an offset:
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"{e.msg} at line {e.lineno}, column {e.colno}")
print(e.doc[max(0, e.pos - 40):e.pos + 40])Checklist
- Failed at char 0? The input was empty, HTML, or not JSON at all — print it.
- Using
requests? Addraise_for_status()before.json(). - “Extra data”? You have NDJSON — parse line by line.
- File looks valid but still fails? Open it with
encoding="utf-8-sig". - Still stuck? Paste it into the JSON validator for the exact line and column.
Frequently asked questions
What does “char 0” mean?
It is the character offset where parsing stopped — zero means the first character. Combined with “Expecting value”, it tells you the input was empty or never JSON to begin with.
Why does my file work in an editor but fail in Python?
Almost always a UTF-8 BOM. Editors hide it; Python does not. Open the file with encoding="utf-8-sig" to strip it.
What is the difference between json.load and json.loads?
json.load takes a file object, json.loads takes a string. Passing a filename string to json.loads tries to parse the filename itself as JSON and fails at char 0.
How do I parse a file with one JSON object per line?
That format is NDJSON, not JSON. Loop over the file and call json.loads on each line separately — a single json.load raises “Extra data”.