How Prefect Handles Timezone-Aware Datetime Conversions with the whenever Library

Prefect centralizes timezone-aware datetime handling in src/prefect/types/_datetime.py, automatically switching to the whenever library on Python 3.13+ while maintaining backward compatibility through abstraction helpers that convert between whenever objects and standard library datetime instances.

The Prefect workflow orchestration library (PrefectHQ/prefect) recently modernized its datetime internals to leverage the whenever package for robust timezone and DST handling. By isolating conversion logic behind version-agnostic utility functions, Prefect ensures that flow runs, schedules, and task executions behave consistently across Python versions. This implementation guarantees correct UTC coercion and DST transitions regardless of whether the underlying runtime uses pendulum or whenever.

Detecting the whenever API Version

Prefect dynamically detects which whenever API is available at import time to determine the appropriate code paths.

The boolean flag _WHENEVER_NEW_API checks for the presence of the to_stdlib method on whenever.ZonedDateTime (lines 15-25 in src/prefect/types/_datetime.py). This detection mechanism allows Prefect to support both legacy and modern whenever interfaces without breaking existing deployments.

When the new API is available, Prefect uses direct method calls like .to_stdlib() for conversions. Otherwise, it falls back to legacy methods such as .py_datetime() and classmethods like from_py_datetime.

Core Conversion Helpers

Prefect implements three primary helper functions to bridge whenever objects and Python's standard library datetime types.

Converting whenever to Standard Library

The _whenever_to_stdlib() function (lines 59-68) handles the conversion of whenever objects to datetime.datetime instances:

from whenever import ZonedDateTime
from prefect.types import _whenever_to_stdlib

zdt = ZonedDateTime.from_isoformat("2024-03-10T02:30:00-05:00")
std_dt = _whenever_to_stdlib(zdt)
print(type(std_dt))  # <class 'datetime.datetime'>

This helper first attempts to use the newer to_stdlib() method when _WHENEVER_NEW_API is True. If unavailable, it gracefully falls back to the older py_datetime() method.

Creating ZonedDateTime Objects

The _whenever_zdt_from_py() function (lines 71-81) constructs whenever.ZonedDateTime instances from standard library datetimes. When the new API is present, it passes the datetime directly to the constructor. On older API versions, it invokes the from_py_datetime classmethod.

Similarly, _whenever_pdt_from_py() (lines 84-94) creates whenever.PlainDateTime objects from naive datetimes using the same pattern detection logic.

Parsing and Validation

Prefect coerces all datetime inputs into timezone-aware objects through centralized parsing and validation utilities.

String Parsing with Timezone Fallback

The parse_datetime() function (lines 82-90) uses dateutil.parse to interpret datetime strings. If the parsed result lacks timezone information, Prefect automatically assumes UTC by applying ZoneInfo("UTC"). For Python versions older than 3.13, this function delegates to the pendulum parser instead.

UTC Coercion on Validation

The create_datetime_instance() validator (lines 98-105) enforces timezone awareness for Prefect's custom DateTime Pydantic type. When it encounters a naive datetime, it automatically inserts UTC timezone information using ZoneInfo("UTC") before returning the instance.

Current Time and Timestamp Handling

Prefect provides unified interfaces for obtaining the current time and converting timestamps that work across both whenever and pendulum implementations.

Getting "Now" in Any Timezone

The now() function (lines 197-210) returns the current datetime in the specified timezone:

from prefect.types import now

utc_now = now("UTC")
ny_now = now("America/New_York")  # Automatically handles DST

For Python 3.13+, this returns datetime.datetime.now(ZoneInfo(tz)). On older versions, it delegates to pendulum.now().

Timestamp Conversion

The from_timestamp() function (lines 9-15) builds timezone-aware datetimes from Unix timestamps. It accepts a timezone parameter and constructs the result using ZoneInfo, ensuring consistent behavior regardless of the underlying datetime library.

Human-Friendly Formatting

The human_friendly_diff() function (lines 19-56) generates natural-language time differences while normalizing timezone handling.

This utility first ensures all datetimes use valid ZoneInfo objects, then delegates to the humanize library on Python 3.13+ or pendulum on older versions. This abstraction allows Prefect to display consistent "2 hours ago" or "in 3 days" messaging across the UI and CLI.

from prefect.types import human_friendly_diff
import datetime

past = datetime.datetime(2023, 5, 1, 9, 0, tzinfo=datetime.timezone.utc)
print(human_friendly_diff(past))  # "2 years ago"

whenever in Server Schemas

Prefect leverages these datetime utilities in server-side components to ensure schedule accuracy.

In src/prefect/server/schemas/schedules.py (lines 181-192), schedule literals are converted into whenever.ZonedDateTime objects. This conversion guarantees correct DST handling for cron schedules and interval-based triggers on Python 3.13+, preventing the ambiguous or non-existent time errors that traditionally plague workflow schedulers during daylight saving transitions.

Summary

  • Prefect detects whenever API capabilities at import time via _WHENEVER_NEW_API to support both legacy and modern interfaces.
  • The _whenever_to_stdlib() and _whenever_zdt_from_py() helpers abstract conversions between whenever objects and standard library datetimes.
  • All naive datetimes are automatically coerced to UTC through create_datetime_instance() validation.
  • The now() and from_timestamp() functions provide version-agnostic ways to get current time and convert timestamps.
  • Server-side schedule schemas use whenever.ZonedDateTime to handle DST transitions correctly on Python 3.13+.

Frequently Asked Questions

What is the difference between whenever and pendulum in Prefect?

whenever is a modern datetime library that provides first-class support for timezone-aware arithmetic and DST handling, while pendulum is the legacy library used in older Python versions. Prefect automatically uses whenever on Python 3.13+ and falls back to pendulum on earlier versions, maintaining identical behavior through abstraction layers in src/prefect/types/_datetime.py.

Does Prefect require Python 3.13 to use whenever?

No, Prefect supports both libraries across Python versions, but whenever is the default implementation only on Python 3.13 and newer. On older Python versions, Prefect continues to use pendulum while providing the same public API and timezone guarantees.

How does Prefect handle daylight saving time transitions?

Prefect uses whenever.ZonedDateTime objects in schedule calculations to properly handle DST transitions. According to the source code in src/prefect/server/schemas/schedules.py, converting schedule literals to ZonedDateTime ensures that cron schedules and interval triggers execute at the correct local time even during spring-forward or fall-back transitions.

Can I use standard library datetime objects with Prefect's DateTime type?

Yes, Prefect automatically converts standard library datetime objects to its internal DateTime type through the create_datetime_instance() validator. If you pass a naive datetime, Prefect assumes UTC. If you pass a timezone-aware datetime, it preserves the timezone information while ensuring compatibility with the underlying whenever or pendulum implementation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →