Functional patterns

Parsing user input

Human input is ambiguous and locale-dependent — never trust a lenient parser with it.

Human date input is inherently ambiguous. 03/04/05 could mean March 4, 2005 in the US (M/D/Y), April 3, 2005 in most of Europe (D/M/Y), or April 5, 2003 in ISO-adjacent contexts (Y/M/D). Even after you resolve the date order, century expansion is a guess. There is no universally correct interpretation — only the one that matches the user’s locale and intent.

Never feed untrusted user input to a runtime’s lenient parser. Instead: know the locale of your UI, accept input in a constrained format or with separate year/month/day fields, parse with an explicit format string, and validate strictly. If the user’s time zone matters (it usually does), ask for it explicitly — don’t default to the server zone.

When in doubt, prefer structured input (a date picker, three separate fields, or a calendar widget) over a free-text field. A free-text field that auto-detects format is a bug waiting for a user from a different locale.

Show the user what you understood

Whatever you parsed, reflect it back to the user — right where they entered it, the moment the value resolves. A free-text field that accepts 03/04/05 should show “→ 4 March 2005” beside it, so a mis-read is caught by the one person who knows the intended date, before the form is submitted.

How prominent this needs to be scales with how much the input leaves to guesswork:

  • Structured input often confirms itself. A calendar date-picker, or three labelled year / month / day fields, already shows its interpretation — the widget is the echo. No extra confirmation needed.
  • Free-form input always needs an explicit echo. The more a single text field has to infer — date order, century, time zone, “next Tuesday” — the more essential it is to render the resolved value back in an unambiguous form (spelled-out month, four-digit year, explicit zone) next to the field.

This is cheap to build and catches what strict validation can’t: an 05/06 that parses cleanly but lands on the wrong month.

How real engines parse these inputs

Python 3.11.2 · Node 22.22.3 — generated at build time.

InputPython (fromisoformat)JavaScript (new Date())
2026-06-05T14:00:00Z 2026-06-05 14:00:00+00:00 2026-06-05T14:00:00.000Z
2026-06-05T14:00:00+02:00 2026-06-05 14:00:00+02:00 2026-06-05T12:00:00.000Z
2026-06-05 14:00:00 2026-06-05 14:00:00 2026-06-05T14:00:00.000Z
2026-06-05 2026-06-05 00:00:00 2026-06-05T00:00:00.000Z
03/04/05 ValueError: Invalid isoformat string: '03/04/05' 2005-03-04T00:00:00.000Z
June 5, 2026 ValueError: Invalid isoformat string: 'June 5, 2026' 2026-06-05T00:00:00.000Z
2026-6-5 ValueError: Invalid isoformat string: '2026-6-5' 2026-06-05T00:00:00.000Z
1780000000 ValueError: Invalid isoformat string: '1780000000' Invalid Date
Fri, 05 Jun 2026 14:00:00 GMT ValueError: Invalid isoformat string: 'Fri, 05 Jun 2026 14:00:00 GMT' 2026-06-05T14:00:00.000Z
2026-06-05T14:00:00 Z 2026-06-05 14:00:00+00:00 Invalid Date

Pitfall: The same string can parse in one engine and fail (or mean something different) in another. Never rely on a runtime’s lenient parser for untrusted input — require an explicit, strict format (see RFC 3339).

Validate strictly, not leniently

Most standard-library parsers are forgiving by design: they accept 13/32/2025, silently roll over to a neighboring month, or assume a two-digit year using an arbitrary pivot. Always use a parser or options that raise on out-of-range values, and always pass an explicit format string rather than relying on auto-detection.

Pitfall: Passing user-supplied date strings to a guessing parser — new Date(str) (JavaScript) or dateutil.parser.parse() (Python) — without specifying a format. They silently accept malformed or ambiguous strings, make locale- and engine-specific assumptions, and give no error when the interpretation is wrong. (Python’s stdlib datetime.fromisoformat() is the opposite trap: not lenient but narrow and version-dependent — before 3.11 it rejected anything that wasn’t exactly isoformat() output, so what it accepts shifts between releases.)

Go deeper: locale, pivot years, and round-trip confirmation

Locale and format order. The D/M/Y vs M/D/Y ambiguity is only resolved by knowing the user’s locale — not the server’s locale, not the Accept-Language header alone, but the locale the user explicitly operates in. Store it as a preference and use it consistently.

Two-digit year pivot. Parsers that accept two-digit years (e.g. 05) use an arbitrary cutoff — often “within 50 years of now”, meaning the answer changes as the server clock advances. Use four-digit years and reject anything shorter.

Wire format after parsing. Once you have a validated value, serialize it as RFC 3339 (e.g. 2005-03-04T00:00:00Z) for any storage or API call — never pass the raw user string downstream. See also 3rd-party data for the mirror problem: consuming dates from an external source.

For the mirror case — how engines handle valid ISO 8601 / RFC 3339 values — see the canonical comparisons on the Python and JavaScript pages.


← Back to all topics