Quick answer
- The position is where the parser gave up, not where you went wrong. Read backwards from it.
- Look at the previous complete value — a missing comma after it is the most common cause.
- Position equal to the string length? The JSON is truncated, not malformed.
- Number with a leading zero (
01,0080)? JSON forbids those — quote it.
The exact error string
Traceback (most recent call last):
File "app.py", line 3, in <module>
data = json.loads(raw)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 17 (char 16)
# the same error surfaces through requests when a response body isn't valid JSON:
requests.exceptions.JSONDecodeError: Expecting ',' delimiter: line 1 column 17 (char 16)
Python raises this when the parser has finished reading a complete value and the next non-whitespace character is neither a comma nor the bracket that would close the current structure. In other words: the JSON was fine right up to that point, and then something appeared that cannot legally follow.
Everything on this page was verified against CPython 3.14.6.
Anatomy of the message
The message carries three coordinates for the same spot, and knowing which to trust saves most of the debugging time:
Expecting ',' delimiter: line 1 column 17 (char 16)
^^^^^^ ^^^^^^^^^ ^^^^^^^^
| | |
| | +-- e.pos — 0-based index into the string
| +------------- e.colno — 1-based column on that line
+--------------------- e.lineno — 1-based line number
The char value is the useful one. It is a zero-based offset you can slice with directly, whereas the column is only meaningful if your JSON has newlines at all — minified JSON is a single line, so "column 84213" tells you nothing you can act on.
The position is where parsing stopped
This is the detail that makes the error feel wrong, and almost no guide mentions it. A missing comma is only noticed when the parser arrives at the next token, which may be on a later line entirely:
{
"a": 1 <-- the comma is missing HERE, at the end of line 2
"b": 2
}
# Expecting ',' delimiter: line 3 column 3 (char 13)
# ^^^^^^ reported on line 3 — the next token, not the omission
So the rule is: go to the reported position, then read backwards to the end of the previous complete value. The problem sits in that gap. Pasting the document into the JSON Formatter or JSON Validator does the same walk for you and highlights the spot.
Eight inputs, one message
This single message covers a surprising range of mistakes. Each row below was run through json.loads() and produced exactly this error — the third column is the character the parser stopped on, which is your fastest clue:
| Malformed input | Real problem | Stops on |
|---|---|---|
{"name": "John" "age": 25} | Missing comma between pairs | " |
[1 2, 3] | Missing comma between array items | 2 |
{"name": "Alice "dev""} | Unescaped quote ended the string early | d |
{"a": 01} | Leading zero — illegal in JSON | 1 |
{"port": 0080} | Zero-padded value that must be a string | 0 |
{"a": 0x1F} | Hex literal — JSON has no hex numbers | x |
{"a": 1 // note} | Comment — JSON has no comments | / |
{"a": 1, "b": {"c": 2 | Truncated — input ended mid-structure | end of input |
Notice how different these causes are. A missing comma is a typo; a leading zero is a data-modelling problem; a truncated document is a network or I/O problem. The message is identical, which is why "add a comma" advice so often fails to help.
Fix 1: read the actual failure region
Stop counting columns by hand. The exception carries the original document, so slice it:
import json
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"{e.msg} at pos {e.pos} (line {e.lineno}, col {e.colno})")
lo, hi = max(0, e.pos - 40), e.pos + 40
print(repr(e.doc[lo:hi])) # e.doc is the exact string you passed in
print(" " * (e.pos - lo + 1) + "^")
Running that on {"users": [{"id": 1, "name": "Ann"} {"id": 2}]} prints the context with a caret under the offending { — the missing comma between the two objects becomes obvious immediately. JSONDecodeError subclasses ValueError, so existing except ValueError handlers already catch it if you would rather not import the specific type.
Fix 2: the missing comma (and where to actually look)
The dominant cause, and usually the result of hand-editing or string concatenation. Because the parser reports the next token, look at the line above the reported one:
# ❌ broken — no comma after the first object
# '[{"id": 1} {"id": 2}]'
# ✅ fixed
'[{"id": 1}, {"id": 2}]'
If you are building JSON by joining strings, stop and use json.dumps() instead — it cannot produce a missing delimiter. Hand-assembled JSON is the single largest source of this error, and every minute spent debugging the output is a minute the serializer would have saved.
Fix 3: an unescaped quote ended the string early
This one is deceptive because the JSON looks balanced. The parser closes the string at the second quote, then finds a bare word where a comma should be:
# ❌ the string ends after "Alice " — then 'dev' is unparseable
# '{"name": "Alice "dev""}'
# Expecting ',' delimiter — stops on 'd'
# ✅ escape the inner quotes
'{"name": "Alice \\"dev\\""}'
In Python source, remember you are escaping twice: once for the Python string literal and once for JSON. Using a raw string (r'...') or, better, letting json.dumps() build the value removes the ambiguity entirely. If the stray character inside your string is a real newline or tab rather than a quote, you get a different message — see Invalid control character.
Fix 4: numbers JSON won't accept
Three number forms that are perfectly ordinary in other languages are illegal in JSON, and all three land on this error. RFC 8259 §6 is explicit: "Leading zeros are not allowed."
# ❌ all three are invalid JSON
# {"a": 01} leading zero
# {"port": 0080} zero-padded port
# {"a": 0x1F} hex literal
# ✅ quote anything whose leading zeros are significant
{"port": "0080", "zip": "01970", "id": "007"}
This is a data-modelling signal, not just a syntax fix. A zero-padded value is a string with a fixed format — a zip code, a port, an account number — and storing it as a number would silently destroy the padding even if JSON allowed it. Quote it at the source.
Fix 5: the JSON is truncated, not malformed
When e.pos equals the length of the string, nothing is wrong with the syntax — the document simply stopped early. The parser hit end-of-input while still waiting for a comma or a closing brace:
except json.JSONDecodeError as e:
if e.pos >= len(e.doc.rstrip()):
print(f"Truncated: got {len(e.doc)} chars, JSON ends mid-structure")
Common origins: a response cut short by a read timeout, a file read while another process was still writing it, a size or memory limit in a serverless function, or a proxy that closed the connection early. Check the length you received against the Content-Length header, and make sure any writer closed and flushed before you read.
Which message you get depends on where the cut landed, which is a useful signal in itself:
| Cut point | Example | Message |
|---|---|---|
| Between values, or mid-structure | {"a": 1, "b": {"c": 2 | This error |
| Inside a string | {"a": "unfinis | Unterminated string starting at |
| Not cut at all — a second document follows | {"a": 1}{"b": 2} | Extra data |
So if you are chasing a truncation bug and the message is Unterminated string, the payload was cut while a string was open — same root cause, different landing point. Both are worth checking against the same byte-count evidence.
What this error is not
Two things are widely misattributed to this message. Both were checked on 3.14.6:
| Input | What actually happens |
|---|---|
{"a": 1,} (trailing comma) | Illegal trailing comma before end of object — its own message, not this one |
[1, 2,] | Illegal trailing comma before end of array |
{"a": NaN} | Parses successfully. Python accepts NaN/Infinity by default |
{'a': 1} (single quotes) | Expecting property name enclosed in double quotes |
The trailing-comma row matters because many guides still list it as a cause of this error. Modern CPython gives it a dedicated message; if the advice you are reading doesn't match the message you actually got, it was written for an older Python.
The NaN row is worth pausing on. Python's parser accepts NaN, Infinity and -Infinity even though RFC 8259 defines no such literals, so a document that Python reads happily may be rejected by every other language in your stack. Pass parse_constant to json.loads() if you need to reject them at the boundary.
Sibling errors in the JSONDecodeError family
| Message | Means |
|---|---|
Expecting ',' delimiter | A complete value was read; what followed couldn't continue the structure |
Expecting value | No value at all where one was required — often HTML or an empty body |
Extra data | A valid document, then more content — usually NDJSON fed to loads() |
Unterminated string starting at | A string opened and never closed |
Expecting ':' delimiter | A key was read but no colon followed |
All of them share the same coordinates and the same e.doc/e.pos slicing trick, so the debugging technique in Fix 1 works across the whole family. Paste a raw traceback into the Error Log Analyzer and it will route you to the right one.
Debugging checklist
- ✓ Slice
e.docarounde.pos— don't count columns by hand - ✓ Read backwards from the position to the previous complete value
- ✓
e.pos== length of input? It's truncated, not malformed - ✓ Check the value before the position for an unescaped
" - ✓ Any number with a leading zero must be quoted
- ✓ Building JSON by string concatenation? Use
json.dumps()instead - ✓ Reading a file? Confirm the writer closed and flushed it first
- ✓ Message doesn't match the advice you're reading? Check the Python version it targets
Frequently Asked Questions
How do I fix JSONDecodeError: Expecting ',' delimiter?
Look at the character at the reported position, then look at what comes immediately before it. The parser finished a complete value and then found something that was neither a comma nor a closing bracket. The most common causes are a missing comma between items, an unescaped double quote that ended a string early, and a number written with a leading zero such as 01. Slice the raw string around e.pos to see the region rather than counting columns by hand.
Why does the reported column point at valid-looking JSON?
Because the position marks where the parser gave up, not where the mistake is. A comma missing at the end of one line is only detected when the parser reaches the next token, so the error is frequently reported on the following line and at a column that looks perfectly fine. Always read backwards from the reported position to the end of the previous complete value; the omission is almost always there.
Why does a leading zero like 01 cause this error?
Because JSON forbids leading zeros in numbers. RFC 8259 states plainly that leading zeros are not allowed, so when Python reads {"a": 01} it parses the 0 as a complete number, then finds a 1 where it expected a comma or a closing brace. Zero-padded values such as ports, zip codes and IDs must be quoted as strings, so write {"port": "0080"} rather than {"port": 0080}.
Does a trailing comma cause Expecting ',' delimiter?
Not on current Python. A trailing comma produces its own message — Illegal trailing comma before end of object, or before end of array — verified on CPython 3.14.6. Older versions reported it less specifically, which is why many guides still list trailing commas under this error. If you are reading advice that does not match your actual message, check which Python version it was written for.
How do I see exactly where the JSON broke?
Catch the exception and slice the original document around the failure position. JSONDecodeError carries msg, pos, lineno, colno and doc, where doc is the exact string you passed in, so printing doc[max(0, pos-40):pos+40] shows the offending region with its surrounding context. This is far more reliable than counting to a column number by hand, particularly in minified JSON that is one enormous line.
What if the error is at the very end of the input?
Then the JSON is truncated rather than malformed. A response cut short by a timeout, a partial file write, or a size limit ends mid-structure, and the parser reports Expecting ',' delimiter with a position equal to the length of the string. Check the byte count you actually received against Content-Length, and confirm that the writer closed and flushed the file before you read it.
Why does Python accept NaN and Infinity but reject my leading zero?
Because Python's json module deliberately extends the standard in one direction while enforcing it in another. NaN, Infinity and -Infinity are accepted by default even though RFC 8259 has no such literals, whereas leading zeros are rejected as the specification requires. If you need strict behaviour, pass parse_constant to raise on those literals, otherwise data that no other language's parser will accept can pass silently through your service.
References
- json.JSONDecodeError (Python Standard Library)
- json.loads (Python Standard Library)
- RFC 8259 §6 — Numbers (IETF)
Find the broken character instantly
Paste the JSON and the formatter points straight at the failure — no column counting.