One type for everything

Python uses a single datetime type to represent two fundamentally different concepts:

  • Naive datetimes, which have no time zone information

  • Aware datetimes, which are associated with a time zone

These two behave differently, are interpreted differently, and should never be mixed. Yet the type system makes no distinction between them.

This becomes especially frustrating in typed code. There is no way to express, using type annotations, whether a function expects a naive or an aware datetime.

def schedule_at(dt: datetime) -> None:
    ...

Does dt represent a local wall-clock time? A UTC timestamp? A zoned time? The type gives you no way to say.

This makes it impossible to statically enforce one of the most important invariants in date-time code. As a result, mistakes that should be caught early often surface only at runtime—or worse, much later.

How whenever solves this

whenever strictly separates datetimes with and without time zone information:

  • PlainDateTime is the equivalent of a “naive” time

  • ZonedDateTime is the equivalent of an “aware” time with ZoneInfo attached

  • Instant is the equivalent of an “aware” time with UTC attached

This makes type annotations precise and self-documenting:

def schedule_at(dt: ZonedDateTime) -> None:
    ...