4.9 KiB
| title | aliases | tags | sources | created | updated | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Python — fromisoformat() Cannot Parse Z Suffix (Python < 3.11) |
|
|
|
2026-04-24 | 2026-04-24 |
Python — fromisoformat() Cannot Parse Z Suffix (Python < 3.11)
JavaScript's Date.toISOString() always appends Z to ISO 8601 strings (e.g., 2026-04-24T11:00:00.000Z). Python's datetime.fromisoformat() cannot parse the Z suffix in Python versions before 3.11 — it raises ValueError. This causes silent 500 errors whenever a JS frontend sends a date string to a Python backend that uses fromisoformat() for parsing.
Key Points
- JS
toISOString()always outputsZsuffix — there is no way to suppress this; every date sent from a browser will haveZ datetime.fromisoformat("2026-04-24T11:00:00.000Z")fails in Python < 3.11 — raisesValueError: Invalid isoformat string- Python 3.11+ fixed this:
fromisoformat()now acceptsZas a valid UTC offset - The failure is silent in API endpoints — the ValueError is caught by the framework as a 500, often with no log output visible in the response
- The filter/query parameters sent from JS date pickers are the most common trigger:
from=2026-04-01T00:00:00.000Z&to=2026-04-30T23:59:59.999Z
Details
The Problem
# Python 3.9 / 3.10 — FAILS
from datetime import datetime
datetime.fromisoformat("2026-04-24T11:00:00.000Z")
# ValueError: Invalid isoformat string: '2026-04-24T11:00:00.000Z'
# Python 3.11+ — WORKS
datetime.fromisoformat("2026-04-24T11:00:00.000Z")
# datetime(2026, 4, 24, 11, 0, tzinfo=timezone.utc)
The Z suffix is valid ISO 8601 for UTC, but Python's stdlib didn't support it in fromisoformat() until 3.11 (PEP 683).
The Fix: Normalize Before Parsing
Replace Z with +00:00 before passing to fromisoformat():
def _parse_iso(s: str) -> datetime:
"""Parse ISO string from JS toISOString() — handles Z suffix across all Python versions."""
if s.endswith("Z"):
s = s[:-1] + "+00:00"
return datetime.fromisoformat(s)
Or use a single-line approach:
from datetime import datetime, timezone
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
Why the Failure Is Silent
In a Quart/Flask/FastAPI endpoint:
@app.route("/api/usage")
async def get_usage():
from_str = request.args.get("from")
from_dt = datetime.fromisoformat(from_str) # raises ValueError if Z suffix
...
The ValueError bubbles up as an unhandled exception → the framework catches it as a 500 Internal Server Error. The frontend receives a 500 with no body (or a generic error page). The filter appears to "not work" — but the API is actually crashing on date parsing.
This is especially insidious because:
- The endpoint works in local dev if dates are typed manually without
Z - The endpoint works in Python 3.11+ without any code change
- The 500 may not appear in application logs if the exception handler swallows it
Real Incident (2026-04-24)
Semblance admin dashboard period selector (React + TypeScript) sent dates via new Date(startDate).toISOString() as query parameters. Every request with a from or to date filter returned 500. The admin usage summary showed 0 rows. Root cause: Quart backend running Python 3.9, datetime.fromisoformat() rejecting Z suffix. Fix: _parse_iso() helper with Z → +00:00 substitution.
Period Filter Fallback Bug
The same endpoint had a related bug: when no from/to params were sent (meaning "All time"), the code defaulted to _month_start() — silently filtering to the current month instead of returning all records. The correct behavior for absent params is no date filter:
def _period_match(from_str, to_str):
if not from_str and not to_str:
return {} # no filter — return all records
filters = {}
if from_str:
filters["created_at__gte"] = _parse_iso(from_str)
if to_str:
filters["created_at__lte"] = _parse_iso(to_str)
return filters
Both bugs together caused the "All time" view to show current-month data and the date filter to always 500.
Related Concepts
- wiki/concepts/preflight-record-pattern — AI cost tracking that uses datetime for
effective_from/tofields - wiki/concepts/openai-max-completion-tokens — another silent API parameter mismatch that causes hard errors
- wiki/tech-patterns/fastapi-python-docker — FastAPI Python backend where date parsing happens at the API boundary
Sources
- daily/2026-04-24.md — Semblance admin dashboard period filter returning 500 on all date-filtered requests; root cause was Python 3.9
fromisoformat()rejectingZsuffix from JStoISOString(); fix:_parse_iso()helper withZ → +00:00substitution; additional bug: missing params defaulted to current month instead of no filter