PythonMastery
reference 3 min read · lesson 29 of 60 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 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

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

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

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}")
    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.

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

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 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 with utf-8-sig instead.
  • 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 because a function has no return.

See Also

Found this useful? Share it with a friend who’s learning Python.