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 a JSON value (string, number, object, array, boolean, null) should be, but found something else, or found the end of the 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. Usually the input is empty or only whitespace, or it starts with something that is not JSON, such as the < of an HTML page.
Why it happens
Strict JSON (RFC 8259) allows less than many people think. Common problems:
| 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, often an HTML error page |
How to fix it
Option 1: print the real content first. Many of these bugs become obvious as soon as you look at the text.
import json from pathlib import Path raw = Path("config.json").read_text() print(repr(raw[:80])) # shows the first 80 characters, with escapes visible 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 the call in try/except and report the 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}") lines = raw.splitlines() if lines: # an empty body has no line to show print(lines[e.lineno - 1]) raise
setup added so this can run · re-runs 1 earlier block on this page first
# 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') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import json from pathlib import Path except Exception: pass _pm_earlier = [ 'import json\nfrom pathlib import Path\n\nraw = Path("config.json").read_text()\nprint(repr(raw[:80])) # shows the first 80 characters, with escapes visible\ndata = json.loads(raw)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Option 3: for "JSON" files that people edit by hand, switch to another format. People who write config files by hand want comments and trailing commas. JSON allows 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 · re-runs 1 earlier block on this page first
# 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()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import json from pathlib import Path except Exception: pass _pm_earlier = [ 'import json\nfrom pathlib import Path\n\nraw = Path("config.json").read_text()\nprint(repr(raw[:80])) # shows the first 80 characters, with escapes visible\ndata = json.loads(raw)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Option 4: for JSON with comments. 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 that the HTTP response really is JSON. Always do this 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 see this in real code
- An API rate-limited you and returned an HTML "429" page, but your code called
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 program wrote a UTF-8 BOM at the start of the file. If you read the file with
utf-8,json.loads(text)fails. Open it withutf-8-siginstead.
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 because a function has no return.
See Also
- All Python errors: the full index, by type and by when it happens.
- CSV and JSON: reading and writing JSON in practice.
- File I/O: safe file reading.
- cheat-json: quick reference.
Found this useful? Share it with a friend who’s learning Python.