json.JSONDecodeError: Expecting value
1 · The lesson
readWhat this error means
json.loads or json.load could not parse the input as JSON. JSONDecodeError is a subclass of ValueError and carries msg, doc, pos, lineno, and colno attributes pointing at where the parser gave up. "Expecting value" is the most common message — it means the parser was at a position where any JSON value (string, number, object, array, boolean, null) could appear, but found something else, or found end-of-input.
When you see it
Traceback (most recent call last):
File "load.py", line 4, in <module>
data = json.loads(text)
File ".../json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)line 1 column 1 (char 0) means the parser failed at the very first character — almost always because the input is empty or whitespace.
Why it happens
Strict JSON (RFC 8259) is narrower than people assume. Common offenders:
| You wrote | Why it fails |
|---|---|
"" (empty string) | not a JSON value |
{'name': 'Ada'} | single quotes are not JSON; use " |
{"name": "Ada",} | trailing comma not allowed |
{"name": "Ada" // comment} | comments not allowed |
{name: "Ada"} | keys must be quoted strings |
NaN, Infinity (in strict) | not in the spec (Python accepts by default) |
200 OK from a server | not JSON at all — likely an HTML error page |
How to fix it
Option 1 — log the actual content first. Half of these bugs evaporate when you look at the bytes.
import json from pathlib import Path raw = Path("config.json").read_text() print(repr(raw[:80])) # see the first 80 chars, escaped data = json.loads(raw)
If repr shows '', the file is empty. If it shows '<!DOCTYPE html>...', the API returned an HTML error page.
Option 2 — wrap in try/except and report position.
import json try: data = json.loads(raw) except json.JSONDecodeError as e: print(f"bad JSON at line {e.lineno} col {e.colno}: {e.msg}") print(raw.splitlines()[e.lineno - 1]) raise
setup added so this can run · defines raw
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) raw = _AutoMock('raw')
Option 3 — for human-edited "JSON", switch format. Hand-written config files want comments and trailing commas. JSON has neither. Use TOML (stdlib tomllib) or YAML (pyyaml) instead.
import tomllib config = tomllib.loads(Path("config.toml").read_text())
setup added so this can run · defines Path
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Path(*_a, **_kw): print('-> Path() called') return _AutoMock('Path()')
Option 4 — for JSON-with-comments specifically. Some configs (VS Code's settings.json, tsconfig.json) use JSONC. Use the json5 or jsonc-parser package, or strip comments before parsing.
Option 5 — check the HTTP response actually was JSON. Always before parsing:
resp = requests.get(url) resp.raise_for_status() if "application/json" not in resp.headers.get("content-type", ""): raise RuntimeError(f"expected JSON, got {resp.headers.get('content-type')}") data = resp.json()
setup added so this can run · defines url, requests
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) url = _AutoMock('url') requests = _AutoMock('requests')
When you'd actually see this in real code
- An API rate-limited you and returned an HTML "429" page; your code did
resp.json()anyway. - A user hand-edited
config.json, left a trailing comma, and the app won't start. - A previous run crashed mid-write and left a half-written, truncated JSON file.
- A Windows export wrote a UTF-8 BOM at the start;
json.loads(text)on the raw bytes chokes — useutf-8-sigwhen opening.
Related errors
UnicodeDecodeError— the bytes can't even be decoded to a string. Fix that first. See error-unicodedecodeerror.TypeError: the JSON object must be str, bytes or bytearray, not NoneType— you passedNone, usually from a missing return.
See Also
- All Python errors — the full index, by type and by when it happens.
- CSV and JSON — practical JSON I/O.
- File I/O — safe file reading.
- cheat-json — fast lookup.