This error means Python's JSON parser received something it cannot start parsing: an empty string, a string of only whitespace, HTML from an error page, or a file with a UTF-8 BOM (byte order mark). The fix is nearly always the same β check what you're actually handing to json.loads() or json.load() before you call it.
What the error looks like
Traceback (most recent call last):
File "fetch.py", line 8, in <module>
data = json.loads(response_text)
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
JSONDecodeError is a subclass of ValueError, so you can catch it with either. The message "Expecting value: line 1 column 1 (char 0)" pinpoints column 1, meaning the parser gave up at the very first character β it never even got started.
The four common causes
1. Empty string or empty file
This is the most frequent cause. An HTTP request returned an empty body, a file was created but never written to, or a variable was initialized to "" and never populated.
$ python3
>>> import json
>>> json.loads("")
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
>>> open("empty.json", "w").close()
>>> json.load(open("empty.json"))
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Fix: Check the string before parsing.
import json
raw = response.text # or file.read()
if not raw or not raw.strip():
raise ValueError("Response body is empty β nothing to parse")
data = json.loads(raw)
2. Server returned HTML instead of JSON
This is the cause that trips people up the most. Your code is working fine, but the server at that URL returned a 404 page, a login redirect, a Cloudflare challenge, or a maintenance page β all HTML. json.loads("<html>...")) fails immediately because < is not valid JSON.
>>> json.loads("<html><body>404 Not Found</body></html>")
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Fix: Check the Content-Type header before parsing, and log the raw body when it fails so you can see what the server actually sent.
import json
import urllib.request
def fetch_json(url: str) -> dict:
with urllib.request.urlopen(url) as resp:
body = resp.read().decode("utf-8")
content_type = resp.headers.get("Content-Type", "")
if not body.strip():
raise ValueError("Empty response body")
if "text/html" in content_type:
snippet = body[:300].replace("\n", " ")
raise ValueError(
f"Server returned HTML, not JSON.\n"
f"Content-Type: {content_type}\n"
f"Body starts with: {snippet!r}"
)
try:
return json.loads(body)
except json.JSONDecodeError as exc:
snippet = body[:300].replace("\n", " ")
raise ValueError(
f"Invalid JSON from server.\n"
f"Body starts with: {snippet!r}"
) from exc
If you use the requests library, the pattern is the same β use resp.headers["Content-Type"] and resp.text. You can also call resp.raise_for_status() before parsing so HTTP 4xx/5xx errors surface immediately rather than after the confused JSON parse attempt.
3. Whitespace-only string
A string of spaces, tabs, or newlines produces a slightly different position in the message but the same error class:
>>> json.loads(" ")
json.decoder.JSONDecodeError: Expecting value: line 1 column 4 (char 3)
The fix is the same as case 1: call .strip() and check the result before parsing.
4. UTF-8 BOM at the start of a file
Files saved by Windows tools (Excel "Save as UTF-8 CSV", Notepad, some editors) sometimes write a three-byte UTF-8 BOM (\xef\xbb\xbf) before the actual content. Python's json.load() sees that byte sequence as an invalid first character and raises a slightly different message:
json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
Python is actually helpful here β it tells you exactly what to do.
# Wrong β fails on BOM files
with open("export.json", encoding="utf-8") as f:
data = json.load(f)
# json.decoder.JSONDecodeError: Unexpected UTF-8 BOM ...
# Right β utf-8-sig silently strips the BOM if present, harmless otherwise
with open("export.json", encoding="utf-8-sig") as f:
data = json.load(f)
Using encoding="utf-8-sig" on all JSON files you read from disk is a safe habit: it strips the BOM when present and does nothing when it is absent.
Quick diagnostic checklist
When you hit this error, run through these in order:
- Print or log the raw string before parsing:
print(repr(raw[:200])). This immediately tells you whether it is empty, HTML, or otherwise unexpected. - If it is empty, find out why the source (API, file, database query) returned nothing.
- If it starts with
<, you got HTML. Check the URL, authentication, and HTTP status code. - If it starts with
\xef\xbb\xbf(shows as a strange leading character), open the file withencoding="utf-8-sig". - If it looks almost like JSON but fails, you may have a trailing comma or a comment β Python's
jsonmodule follows strict RFC 8259 and rejects both. Use a lenient parser likejson5or clean the string first.
Putting it together: a reusable safe parse helper
import json
from typing import Any
def safe_json_loads(text: str, source: str = "response") -> Any:
"""
Parse a JSON string with a clear error message when input is empty or not JSON.
Raises ValueError with diagnostic detail instead of a bare JSONDecodeError.
"""
if text is None:
raise ValueError(f"{source} was None β check that the call succeeded")
stripped = text.strip()
if not stripped:
raise ValueError(f"{source} body is empty")
if stripped.startswith("<"):
raise ValueError(
f"{source} looks like HTML, not JSON. "
f"First 200 chars: {text[:200]!r}"
)
try:
return json.loads(stripped)
except json.JSONDecodeError as exc:
raise ValueError(
f"Invalid JSON in {source}. "
f"First 200 chars: {text[:200]!r}"
) from exc
Using this instead of bare json.loads() turns a cryptic "Expecting value: line 1 column 1" into an actionable message that shows you the actual content that was received.
How to confirm the fix worked
>>> safe_json_loads("")
ValueError: response body is empty
>>> safe_json_loads("<html></html>")
ValueError: response looks like HTML, not JSON. First 200 chars: '<html></html>'
>>> safe_json_loads('{"status": "ok"}')
{'status': 'ok'}
Tested on Ubuntu 24.04 with Python 3.12.3, GCC 13.3.0.