PythonMastery
reference 3 min read · lesson 29 of 45 in Errors

json.JSONDecodeError: Expecting value

1 · The lesson

read

What 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

text
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 wroteWhy 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 servernot 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.

python
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.

python
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.

python
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:

python
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 — use utf-8-sig when opening.
  • 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 passed None, usually from a missing return.

See Also