Expecting value: line 1 column 1 (char 0) — a Python-first field guide
line 1 column 1 (char 0) means Python failed on the first character — so the input was not JSON at all. Here is how to read the message and fix every cause, from empty bodies to HTML error pages and BOMs.
If you are here, you almost certainly ran json.loads(something) or response.json() and Python threw this at you:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
The one thing to understand before anything else: line 1, column 1, char 0 means the parser failed on the very first character it looked at. It never got going. So this is almost never a "my JSON has a typo deep inside" problem — it is a "what I handed the parser was not JSON to begin with" problem. That distinction decides where you look, and it is why the usual instinct (hunt for a missing comma) wastes time here.
Reading the message literally
Python's decoder reports three coordinates, and each one is worth decoding:
- Expecting value — the parser was at a position where a JSON value (object, array, string, number,
true/false/null) must begin, and found something that cannot start one. - line 1 column 1 — the human-readable position. Column 1 of line 1 is the start.
- (char 0) — the zero-based character offset.
char 0is the first byte. If you ever see a different number here — saychar 27— the input was partly valid and failed later; that is a genuinely different bug (trailing junk, a bare value after a complete one) and most of this guide does not apply. The classic, high-volume case ischar 0.
So the message is not vague. It is saying: at the first character, I expected a value and there wasn't one. The fastest possible debugging step is one line:
print(repr(text[:50]), "len=", len(text))
Use repr(), not print(text). repr shows you quotes, escape sequences, a leading BOM, and whether the string is empty — all the things a plain print hides. Nine times out of ten the answer is visible in that one line, and you can paste the fragment into the line 1 column 1 inspector to confirm what the first bytes actually are.
The causes, ranked by how often they actually happen
1. The string is empty ('')
json.loads('') raises this immediately — there is no value at char 0 because there is no char 0. In real code the empty string almost always arrives from a network call that returned no body: a 204 No Content, a 304, a redirect you followed, a HEAD request, or an endpoint that failed before writing anything. With requests:
r = requests.post(url)
data = r.json() # Expecting value: line 1 column 1 (char 0) when r.text == ''
The fix is to treat "no body" as an expected outcome, not an exception:
r = requests.post(url)
data = r.json() if r.status_code != 204 and r.content else None
Tell-tale from the probe line: len= 0.
2. It is an HTML (or plain-text) error page, not JSON
This is the sneaky one and, in production, often the real top cause. Your request hit a login wall, a 500 page, an Nginx 502 Bad Gateway, or a proxy that returned HTML. The body is non-empty, the HTTP status might even look fine after a redirect, and the first character is < — the start of <!DOCTYPE html>. The parser sees < at char 0, which cannot start a JSON value, and throws.
Tell-tale from the probe line: repr(text[:50]) starts with '<!DOCTYPE' or '<html' or '<?xml'. The fix is to check the response before trusting it:
r = requests.get(url)
r.raise_for_status() # turn a 4xx/5xx into an exception
ctype = r.headers.get("content-type", "")
if "application/json" not in ctype:
raise ValueError(f"Expected JSON, got {ctype}: {r.text[:200]!r}")
data = r.json()
3. There is a UTF-8 BOM in front of the JSON
Files exported from Windows tools, Excel, or some APIs begin with a byte-order mark, U+FEFF. The visible text looks like perfect JSON starting with {, but the actual first character is the invisible BOM, and json.loads refuses it at char 0.
Tell-tale: repr(text[:50]) shows '{...'. The fix is to decode with the BOM-aware codec, or strip it:
# reading a file
text = open("data.json", encoding="utf-8-sig").read() # utf-8-sig eats the BOM
# already have a string
data = json.loads(text.lstrip(""))
4. You passed a Python object, a path, or None — not JSON text
Two very common slips: calling json.loads on something that is already a dict/list (you wanted it parsed, but it was parsed already), or handing it a file path instead of the file's contents.
json.loads("data.json") # tries to parse the literal string "data.json" -> char 0 is 'd'
json.loads(open("data.json")) # a file object, not text
Tell-tale: the probe line shows your filename, or a TypeError instead. Use json.load(open(path)) (note: load, not loads) to read from a file object, and never loads a path string.
5. It is Python repr or almost-JSON, not real JSON
Text copied from a Python shell or a log often looks like JSON but uses single quotes, None/True/False, or trailing commas. Single quotes at char 0 ('{' is fine, but "'" as the first char from a quoted key) can trip it; so can a leading stray character. For data you trust and generated yourself in Python, use ast.literal_eval instead of json.loads — it understands Python literals. For everything external, fix the producer to emit real JSON.
A 60-second decision path
- Run
print(repr(text[:50]), "len=", len(text)). len= 0→ empty body. Fix the request/status handling (cause 1). Done.- Starts with
'<'→ it is HTML/XML, an error page. Check status and content-type (cause 2). Done. - Starts with
''→ BOM. Read asutf-8-sig(cause 3). Done. - Shows a filename or a
dict→ you passed the wrong thing (cause 4). Done. - Looks like JSON but with single quotes /
None→ it is Python repr, not JSON (cause 5). - Only if none of these and the offset is not char 0 → you have a real syntax error later in the document; switch to a validator that points at the offending position.
Why requests' .json() makes this worse
response.json() is just json.loads(response.text) with the same decoder underneath, so it raises the identical JSONDecodeError — but it hides the body from you. The habit that removes this whole class of bug: never call .json() blind. Call raise_for_status() first, confirm the content-type, and on failure log response.text[:200]. Most "Expecting value" incidents in logs are an HTML 500 page that .json() swallowed into a stack trace with no clue about what the body was.
Stopping it for good
Three habits retire this error permanently: (1) treat an empty or non-2xx response as a normal branch that returns None or raises a typed error, never a thing you parse; (2) assert the content-type before decoding, so an error page can never masquerade as data; (3) when you read files that might come from Windows or Excel, default to encoding="utf-8-sig" so a BOM never reaches the parser. When you just want to see what the first bytes of a stubborn string really are, the char-0 inspector runs in your browser, reveals hidden BOMs and whitespace, and tells you in one glance whether you were ever holding JSON at all.
FAQ
Does "line 1 column 1 (char 0)" mean the error is at the start of my JSON?
Yes — char 0 is the very first character. The parser failed before reading any value, which means the input was not JSON at all (empty, an HTML error page, a BOM, or the wrong variable), not that there is a typo inside otherwise-valid JSON.
Why does requests' response.json() raise this even on a 200 response?
After a redirect or from a misbehaving proxy a 200 can carry an HTML body. response.json() calls json.loads on that HTML, sees '<' at char 0, and raises. Call raise_for_status(), check the content-type, and log response.text[:200] before parsing.
My file looks like valid JSON but still fails at char 0 — why?
It almost certainly starts with an invisible UTF-8 BOM (U+FEFF) from a Windows/Excel export. Open the file with encoding='utf-8-sig', or strip it with text.lstrip('\ufeff') before json.loads.
How do I debug it in one line?
Run print(repr(text[:50]), 'len=', len(text)). repr reveals emptiness, a leading BOM, quotes and HTML tags that a plain print hides, and the length tells you instantly whether the body was empty.
The offset is not char 0 — is it the same problem?
No. A non-zero offset means the input started as valid JSON and failed later (trailing data, a second bare value, a real syntax error). That is a different bug; use a validator that points at the reported position rather than checking the first bytes.