Parsing and formatting dates safely
Why 03/04/2026 is unparseable without a convention, how strict parsers prevent silent errors, and how to emit locale-independent output for machines.
Ambiguous input and lenient parsers
03/04/2026 is 3 April in most of the world and 4 March in the United States. A parser that just works on this input is guessing, and it will guess differently from the parser next to it.
new Date("03/04/2026") // parsed as March 4 (US, month first)
new Date("2026-03-04") // parsed as March 4 (ISO, but see the trap below)
new Date("2026-03-04T00:00") // interpreted as LOCAL time, not UTC
new Date("2026-03-04") // interpreted as UTC midnight
// two forms that look alike produce instants a day apart- Date-only strings in JavaScript are treated as UTC; date-time strings without a zone are treated as local.
- Python's
datetime.fromisoformatrejects most non-ISO input, which is a feature. - A lenient parser silently shifts a date by a day rather than raising, and the error survives into the database.
Parse strictly, format explicitly
from datetime import datetime, timezone
# explicit format, no guessing
dt = datetime.strptime("2026-09-18 10:30", "%Y-%m-%d %H:%M")
try:
datetime.strptime("18/09/2026", "%Y-%m-%d")
except ValueError as e:
print("rejected:", e) # time data '18/09/2026' does not match format
# machine output: always ISO 8601 with an offset
print(dt.isoformat()) # 2026-09-18T10:30:00
print(dt.replace(tzinfo=timezone.utc).isoformat()) # ...+00:00| Goal | Safe approach | Avoid |
|---|---|---|
| Accept user input | Explicit format list, reject on mismatch | A general-purpose fuzzy parser |
| Store in a database | A datetime type, or ISO 8601 text with offset | Locale-formatted strings |
| Show to a user | Locale-aware formatter with a stated zone | Manual string concatenation |
| Log | ISO 8601 UTC | Local time without a zone |
| Compare or sort | Parse to an instant, then compare | String comparison of non-ISO formats |
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# a naive datetime is a bug waiting to happen: no zone, no instant
naive = datetime(2026, 9, 18, 10, 30)
# attach a zone explicitly before any conversion
aware = naive.replace(tzinfo=ZoneInfo("Europe/Paris"))
utc = aware.astimezone(timezone.utc)
# subtract aware datetimes; subtracting naive ones across zones is meaningless
delta = datetime.now(timezone.utc) - utcFormatting for people and for machines
const instant = new Date("2026-09-18T10:30:00Z");
// for a human in a known locale and zone
new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium", timeStyle: "short",
timeZone: "Europe/Paris",
}).format(instant);
// "18 Sept 2026, 12:30"
// for a machine: never use toLocaleString
instant.toISOString(); // "2026-09-18T10:30:00.000Z"💡
Keep two formatting paths: one for humans, which is locale and zone dependent, and one for machines, which is always ISO 8601 in UTC. Mixing them produces log files that cannot be sorted and UI dates that shift by a day near midnight.
FAQ
Why is my date one day off after saving?
Almost always a time-zone conversion on a date-only value. A birthday has no zone; converting it through UTC midnight moves it to the previous day for negative offsets. Store date-only values as date-only types.
Should I use a library for parsing?
Use the platform parser for ISO 8601, and a library when you must accept several human formats. Whichever you choose, list the accepted formats explicitly and reject everything else.
Related
ISO 8601 and RFC 3339 formats Storing dates in databases
Last refreshed 2026-09-18.