Python "JSONDecodeError: Expecting value"
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) is one of the most common tracebacks in Python, and one of the most misread — it looks like a JSON syntax problem, but "line 1 column 1 (char 0)" is telling you the parser failed on the very first character. That's a much narrower, more specific class of bug than a malformed document.
// what "char 0" actually tells you
json.loads() — an API returned nothing, a file was blank, a variable was never populated401/404/500 page, or a login redirect — the body is HTML, not JSON, and it starts with < or plain text, not { or [204 No Content responsethe request succeeded and there's deliberately no body at all — calling response.json() on it is calling json.loads('')json.loads()json.loads() parses a JSON string; json.load() parses an open file object. Passing a path string to loads() tries to parse the path itself as JSON, which is never valid JSON and always fails at char 0json.loads() call and look at what's actually there; the fix is almost always upstream of the parse call, not in it.data = json.loads(open("config.json"))json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
open("config.json") returns a file object, not a string. json.loads() tries to parse that object's repr -- something like <_io.TextIOWrapper name='config.json' ...> -- as JSON, and fails at the first character.with open("config.json") as f: data = json.load(f) # load(), not loads() -- takes the file object directly
// guarding a real HTTP call against this
For the API-response version of this bug, check before parsing rather than catching the exception after the fact — it's the difference between knowing exactly what came back and just knowing something went wrong:
resp = requests.get(url) resp.raise_for_status() # turn 4xx/5xx into an exception here, not a confusing parse error later if not resp.text.strip(): raise ValueError(f"{url} returned an empty body") data = resp.json() # or: json.loads(resp.text)
// when it's a real position, not char 0
If the line/column/char numbers point somewhere other than the very start, you do have actual JSON that's genuinely malformed partway through — a trailing comma, an unterminated string, a stray invisible character copied in from somewhere else. That's a different, narrower problem than this guide covers, and the JSON Formatter will point at the exact spot and auto-fix what it can.
// frequently asked questions
What does "Expecting value: line 1 column 1 (char 0)" actually mean?
The parser found nothing to even start on — this is different from a real syntax error, which points somewhere inside the document at whatever token it choked on. Char 0 means whatever reached json.loads() wasn't JSON at all, not that it was malformed JSON.
What usually causes JSONDecodeError at char 0?
Four common causes, roughly in order of frequency: empty input reaching json.loads(); a non-JSON HTTP response (a 401/404/500 error page or login redirect, which is HTML not JSON); a 204 No Content response with deliberately no body; or a file path string passed to json.loads() instead of a file object to json.load().
Why does json.loads(open("file.json")) fail with this error?
open() returns a file object, not a string. json.loads() tries to parse that object's repr — something like <_io.TextIOWrapper name='file.json' ...> — as JSON and fails at the first character. Use json.load(f) (no "s") with an open file object instead.
How do I guard an API call against this error?
Check before parsing rather than catching the exception after: call resp.raise_for_status() to turn 4xx/5xx into an exception immediately, and check resp.text.strip() isn't empty before calling resp.json() — this tells you exactly what came back instead of just that something went wrong.
A real JSON syntax error — a missing comma, an unquoted key — points somewhere inside the document, at whatever token the parser choked on. Char 0 means the parser found nothing to even start on. In practice that means one of these, roughly in order of how often each actually shows up: