Python Error Guide: 'json.decoder.JSONDecodeError: Expecting value' — Fix JSON Parsing
Fix 'json.decoder.JSONDecodeError: Expecting value' in Python: handle empty responses, non-JSON error pages, single quotes, trailing commas, and BOM encoding.
- #python
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
json.decoder.JSONDecodeError: Expecting value is raised by json.loads()/json.load() when the parser reaches a point where a JSON value should begin but finds something that can’t start one. The most common form points at the very first character, meaning the input wasn’t JSON at all:
Traceback (most recent call last):
File "fetch.py", line 12, in <module>
data = json.loads(resp.text)
File ".../json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
The line, column, and char in the message point at exactly where parsing failed. line 1 column 1 (char 0) almost always means the string was empty or its first byte wasn’t valid JSON — an error page, a plain-text message, or nothing at all.
Symptoms
json.loads()fails atline 1 column 1 (char 0)— the body is empty or not JSON.- An API call intermittently returns valid JSON but fails when the endpoint errors and returns HTML or plain text.
- Parsing a file works locally but fails in automation because the file is empty or half-written.
- The error points deep into the string (e.g.
line 5 column 12) at a PythonNone/True, a single quote, or a trailing comma. - Reading command output with
json.loads(subprocess...)fails because the command wrote a warning to stdout or produced no output.
Common Root Causes
- Empty input — an empty string, an empty file, or a response with no body (HTTP 204, a timeout, or an error with no content).
- Non-JSON body — an HTTP error returned an HTML error page, a plain-text message, or a redirect, not JSON.
- Leading noise on stdout — a CLI printed a log line, warning, or progress text before/around the JSON you tried to parse.
- Python-style, not JSON — single quotes instead of double quotes,
None/True/Falseinstead ofnull/true/false, or a trailing comma (this isrepr()/Python dict syntax, not JSON). - Double-encoded or already-decoded — passing an already-parsed
dicttojson.loads, or JSON wrapped in extra quotes. - BOM or encoding issues — a UTF-8 BOM (
) at the start, or bytes decoded with the wrong codec. - Concatenated JSON / JSON Lines — multiple JSON objects back to back;
json.loadsonly parses one value. - Truncated output — a partially written file or a stream cut off mid-object.
Diagnostic Workflow
The golden rule: look at the raw bytes before parsing. Print the length and a repr so empty strings and hidden characters are visible:
raw = resp.text
print("len:", len(raw))
print("repr head:", repr(raw[:80])) # repr reveals '', '', '<html>', quotes
If length is 0, you have empty input — the real problem is upstream (the request failed or returned no body). Check the HTTP status and content type before trusting the body:
print("status:", resp.status_code)
print("content-type:", resp.headers.get("content-type"))
For a file, confirm it isn’t empty or truncated and inspect the first bytes:
wc -c data.json # 0 bytes = empty
head -c 100 data.json | od -c # BOM shows as 357 273 277; HTML shows as < h t m l
For subprocess output, separate stdout from stderr so log noise doesn’t pollute the JSON:
import subprocess, json
proc = subprocess.run(["mytool", "--json"], capture_output=True, text=True)
print("stdout repr:", repr(proc.stdout[:120])) # is there a banner before the JSON?
data = json.loads(proc.stdout)
Pinpoint the offending character when the error is mid-string, using the message’s char offset:
try:
json.loads(raw)
except json.JSONDecodeError as e:
print(f"failed at line {e.lineno} col {e.colno} (char {e.pos})")
print("context:", repr(raw[max(0, e.pos - 20): e.pos + 20]))
Example Root Cause Analysis
A monitoring script polled a service’s JSON status endpoint and failed sporadically at 3 a.m.:
Traceback (most recent call last):
File "status.py", line 15, in <module>
payload = json.loads(resp.text)
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
line 1 column 1 (char 0) said the body was empty or non-JSON. Logging the raw response before parsing revealed the truth:
status: 502
content-type: text/html
len: 173
repr head: '<html>\r\n<head><title>502 Bad Gateway</title></head>\r\n<body>...'
During a nightly restart the upstream returned a 502 Bad Gateway HTML page, not JSON. The code trusted resp.text unconditionally and tried to parse an HTML error page. The fix validates the response before decoding and handles the failure gracefully:
import json
import requests
resp = requests.get(url, timeout=10)
# 1. Fail fast on HTTP errors instead of parsing an error page
resp.raise_for_status()
# 2. Confirm the server actually sent JSON
ctype = resp.headers.get("content-type", "")
if "application/json" not in ctype:
raise ValueError(f"Expected JSON, got {ctype!r}: {resp.text[:200]!r}")
# 3. Parse with a clear error that includes the offending body
try:
payload = resp.json()
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON at char {e.pos}: {resp.text[:200]!r}") from e
Now a 502 raises a clear HTTPError (retryable), a wrong content type is rejected with the body shown, and genuinely malformed JSON reports where it broke — instead of a bare Expecting value with no context.
Prevention Best Practices
- Validate before you parse — check the HTTP status (
raise_for_status()) andcontent-typebefore calling.json(), so an error page never reaches the JSON parser. - Guard against empty input — treat empty strings/files/bodies explicitly (
if not raw: ...) instead of letting them hitjson.loads. - Log the raw body on failure — always include
repr(raw[:200])in the error so the next person sees what was actually returned. - Never parse Python
repr()as JSON — single quotes,None/True/False, and trailing commas are not JSON; usejson.dumpsto produce data and, for internal Python structures,ast.literal_eval(nevereval). - Handle BOM and encoding — decode with
utf-8-sigto strip a BOM, and be explicit about the codec when reading bytes. - Use JSON Lines for streams — if a source emits many objects, parse line by line (
json.loads(line)), not the whole blob at once. - Set timeouts and retries — a hung or failed request often yields an empty body; time-box requests and retry transient failures rather than parsing garbage.
Quick Command Reference
print(len(raw), repr(raw[:80])) # spot empty/HTML/BOM input before parsing
resp.raise_for_status() # fail on HTTP errors, don't parse error pages
resp.headers.get("content-type") # confirm it's application/json
json.loads(raw) # parse (wrap in try/except JSONDecodeError)
open(p, encoding="utf-8-sig") # strip a UTF-8 BOM on read
ast.literal_eval(s) # safely parse a Python literal (NOT eval)
wc -c file.json # 0 = empty file
head -c 100 file.json | od -c # reveal BOM / HTML / truncation
Conclusion
JSONDecodeError: Expecting value means the parser found something where a JSON value should start — and line 1 column 1 (char 0) almost always means the input was empty or wasn’t JSON at all, typically an HTML error page or a failed/empty response. The fix is to stop trusting the body blindly: check the HTTP status and content type first, guard against empty input, and always log repr() of the raw bytes when parsing fails. Validate before you parse and the error becomes a clear, actionable message instead of a mysterious Expecting value.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.