Skip to content

domain/timezone — IANA timezone validation

backend/src/domain/timezone.py (55 lines). Shared validation for the two trust boundaries that accept an inbound IANA timezone name — POST /auth/signup and PUT /users/me/timezone — so their rules cannot drift apart (timezone.py:1-8).

Symbol Behavior
MAX_TIMEZONE_LENGTH = 64 Cap matching the User.timezone column width; IANA names are at most 33 chars today (America/Argentina/ComodRivadavia), 64 leaves headroom (timezone.py:14-16)
coerce_timezone_input(value) -> str \| None None for missing / non-string / empty / whitespace-only input (fall back to default); otherwise the trimmed string (timezone.py:19-29)
check_timezone_resolves(candidate) -> None Raises ValueError if over 64 chars or unknown to zoneinfo (timezone.py:32-41)
normalize_timezone(value, default) -> str The composed boundary rule — see below (timezone.py:44-55)

The composed rule (backend/src/domain/timezone.py:44-55):

def normalize_timezone(value: object, default: str) -> str:
    """Coerce, default, and validate an inbound timezone value.

    Blank / missing input returns ``default``; otherwise the trimmed name is
    validated and returned, raising ``ValueError`` on an unknown or oversized
    name so the trust boundary surfaces a 422 instead of storing bad data.
    """
    candidate = coerce_timezone_input(value)
    if candidate is None:
        return default
    check_timezone_resolves(candidate)
    return candidate

Worked examples

Input default="UTC" result
None, "", " ", 42 "UTC"
" America/Los_Angeles " "America/Los_Angeles"
"Mars/Olympus_Mons" ValueError: unknown IANA timezone → 422 at the boundary
65+ char string ValueError: timezone must be 64 chars or fewer

The stored zone is then read by domain/dates for all local-day math (backend/src/models/user.py:50-56).


Grounded in adepthood@fbc529d, 2026-07-31.