Changelog

0.11.0 (2026-10-02)

This release is intended as a soft 1.0 release: it establishes the planned 1.0 API while retaining compatibility shims for newly deprecated interfaces. Unless significant issues arise, this API will become 1.0 after those deprecated interfaces are removed.

Getting the API right for 1.0 means breaking a few things first. Two changes you’ll notice: the system time zone API, which now uses a PEP 661 sentinel, and a round of renames that gives the library one consistent vocabulary.

Thanks for bearing with the churn. This is the last of it: once 1.0 locks in, the API stays put, and releases bring only bug fixes and new features.

This release already brings plenty of the former. It irons out a long list of bugs, most of them in corners: daylight saving time transitions, unusual time zone files, the ends of the supported range, and inputs on which the two backends disagreed. The documentation got a thorough polish too, with a new glossary and a comparison with Arrow.

Breaking changes

  • The system time zone is now accepted everywhere a named time zone (i.e. tz=) is accepted, using the new SYSTEM_TZ sentinel (PEP 661). The methods specific to the system time zone are deprecated: to_system_tz(), assume_system_tz(), Date.today_in_system_tz(), ZonedDateTime.now_in_system_tz(), and ZonedDateTime.from_system_tz().

    ty, pyrefly and mypy 2.4 and later type-check SYSTEM_TZ.

    Rationale: one sentinel lets the regular time zone APIs cover the system time zone without duplicating every operation. It also makes call-time system time zone resolution explicit and takes advantage of the sentinel pattern recently standardized by PEP 661.

  • Several public names are clarified, and the old spellings are deprecated. See the migration table at the end of this entry.

    Rationale: the new names describe their concepts more precisely and use one vocabulary across the API. TZPATH was a module attribute that was loaded on access; its replacement, get_tzpath(), is explicit about this.

  • Removed APIs deprecated before 0.11: DateDelta, DateTimeDelta, the years(), months(), weeks(), and days() helpers, legacy standard library conversion methods, TimeDelta.in_*() convenience methods, Date.days_since() and Date.days_until(), deprecated Date operators, parse_strptime(), ZonedDateTime.start_of_day(), ignore_dst, and ImplicitlyIgnoringDST.

    See the 0.10.0 entry below for migration instructions.

  • Timestamp APIs are consolidated around a unit= argument. Instant.from_timestamp(..., unit=) and exact-time .timestamp(unit=) support seconds, milliseconds, microseconds, and nanoseconds. timestamp_millis(), timestamp_nanos(), and their matching Instant factories are deprecated, as are the timestamp factories on OffsetDateTime and ZonedDateTime: construct an Instant first, then call to_fixed_offset() or to_tz().

    Rationale: one unit-selectable API is easier to discover and extend, while Instant is the natural type for constructing an exact time from a timestamp.

  • Resolving a repeated or skipped local time without an explicit disambiguation= now emits ImplicitDisambiguationWarning. The "compatible" default still applies, so the result is unchanged, but a warning filter set to error now raises. Pass disambiguation="compatible" to silence the warning.

    Rationale: an unstated disambiguation silently picks one of two instants. A sensible default is fine, but the choice should be explicit.

  • Fixed-offset arguments passed as bare integers signifying hours are deprecated. Use TimeDelta or a factory like hours() instead.

    Rationale: the unit of a bare integer is implicit, which makes offset values easy to misread or misuse.

  • Patterns use H/HH for the 24-hour clock; h/hh are deprecated. Optional seconds go in brackets after mm: [:ss], [:ss.fff], [:ss.FFF], or [ss] without a separator. The SS spellings remain with deprecation warnings through 0.11. Patterns and parsed input strings are ASCII-only.

    Rationale: H/HH is the near-universal spelling for a 24-hour specifier, and brackets make optional seconds and their separator explicit. Where brackets may appear is set out in the pattern reference.

  • Unpickling a ZonedDateTime preserves its instant under the time zone rules of the loading environment. If those rules give a different offset, the local time follows and a PickleOffsetMismatchWarning is emitted.

    Rationale: time zone databases change, and unpickling can’t take parameters, so preserving the instant is the sensible default. The warning ensures it doesn’t pass silently.

  • Constructing a ZonedDateTime from a standard library datetime resolves its offset the way ISO parsing does: an offset that disagrees with the time zone rules raises InvalidOffsetError by default, and offset_mismatch= and disambiguation= resolve it.

  • Pattern offsets without seconds round an offset with seconds to the nearest minute, as Temporal does. Hour-only x/X patterns reject offsets that still have minutes after rounding.

  • An ItemizedDelta and an ItemizedDateDelta with the same components are equal: ItemizedDelta(days=3) == ItemizedDateDelta(days=3). strict_eq() takes only a delta of its own type, and raises TypeError for the other.

  • Itemized delta add() and subtract() are simplified: they compose, and take no reference. relative_to= is deprecated, with in_units=, round_mode=, round_increment=, naive_arithmetic_ok=, and stale_offset_ok=; call in_units() on the result instead. The warning is now more precise: CalendarUnitCompositionWarning is now MonthCompositionWarning, escaped with month_composition_ok=, and only years and months trigger it.

    Rationale: relative_to= was in_units() combined with add(), and it silenced the warning without avoiding the actual pitfall: it summed the deltas before applying them, as composition always does.

  • ItemizedDateDelta.in_units() returns an ItemizedDateDelta whatever the reference, where a datetime reference gave an ItemizedDelta.

  • Two lossy calls now warn: Date() given a datetime, whose time it drops (call .date() first), and OffsetDateTime.since()/until() where a calendar-unit result depends on the offset held fixed (StaleOffsetWarning).

  • A few meaningless arguments are rejected: a time with a tzinfo in Time(), a "week" rounding unit anywhere but TimeDelta.round(), an increment= alongside a TimeDelta unit in round(), and pattern fractions followed by a digit field. A non-integer increment= raises TypeError, as do bytes given to YearMonth.parse_iso() and IsoWeekDate.parse_iso(), and a .FFF pattern no longer parses a bare trailing dot. reset_tzpath() takes only a list or tuple: the search path is ordered, and a set gave a different one in each process.

Added and improved

  • Time zone IDs are matched case-insensitively in ASCII letters. The database spelling is used in attributes, representations, ISO output, and pickles, and aliases such as US/Eastern are preserved.

  • patch_current_time() and its TimePatch handle, with shift() and move_to(), are now part of the stable API.

  • Added offset_mismatch= to ZonedDateTime parsing. A numeric offset is matched at its written precision, while Z always identifies an exact UTC instant. OffsetDateTime.assume_tz() supports the same policy and uses disambiguation= when retaining local time.

  • Added Date.today(tz), YearMonth.add() and subtract(), and day_of_week() on PlainDateTime, OffsetDateTime, and ZonedDateTime.

  • Added millisecond and microsecond totals to datetime differences and ItemizedDelta.total(). ItemizedDelta and ItemizedDateDelta are hashable, so they can be set members and dict keys.

  • in_units() and total() on ItemizedDelta and TimeDelta with a PlainDateTime or OffsetDateTime as relative_to= accept the escape their warning names: naive_arithmetic_ok= or stale_offset_ok=.

  • Integer-only arguments accept integer-like objects such as numpy integers.

  • Added LLM-friendly Markdown documentation, including llms.txt.

  • A source install selects the pure-Python backend with the build config setting rust-extension=skip, which configuration files can set: config-settings-package in uv’s pyproject.toml, --config-settings in requirements.txt. WHENEVER_NO_BUILD_RUST_EXT still works.

  • TimeDelta() accepts timedelta subclasses and ZonedDateTime() a datetime with a ZoneInfo subclass, like the other standard library overloads.

  • Added the type aliases DeltaTotalUnitStr and TimestampUnitStr. from whenever import * now includes all the *Str aliases.

Fixed

  • The two backends now agree: they reject the same inputs with the same exception, and give the same result in the few places they differed.

  • Day and hour boundaries around daylight saving time transitions are consistent. start_of(), end_of(), round(), day_length(), and since()/until() agree on where a day starts when a gap or a repeated midnight gets in the way, and units no longer overlap in a fold.

  • ZonedDateTime.replace() and calendar add()/subtract() could jump to the other occurrence of a repeated time, even when it wasn’t necessary. They now keep the current offset where it’s still valid, as parsing does.

  • Rounded differences carry into larger units: rounding up to 1 day and 24 hours gives 2 days, and 12 months gives 1 year. With exact units, round_increment= now rounds the smallest unit, as it does with calendar units.

  • A difference in calendar units across a skipped day (Samoa, 2011) could raise or return a wrong result.

  • A corrupt time zone file raises TimeZoneNotFoundError, where it could crash, fall back to another zone’s rules, or load and give wrong results.

  • POSIX TZ strings follow RFC 9636 on both backends, including edge cases such as daylight saving time all year and rules that cross the new year.

  • The system time zone keeps its ID when TZ is a path into a zoneinfo directory, or when /etc/localtime is a copy of a database file named by /etc/timezone.

  • An empty TZ variable, or no TZ and no /etc/localtime, means UTC, as it does for the C library, instead of raising.

  • On Windows and other platforms that use tzlocal, reset_system_tz() actually determines the system time zone again.

  • Warnings point at your code rather than at internal frames, and a call that rejects an argument no longer warns before it raises.

  • Rare inputs near the ends of the supported range, and time zone files without transitions, could crash the interpreter or give a wrong value.

  • The Rust extension’s round() rounded to the wrong side in a few rare cases.

  • Multiplying or dividing a TimeDelta by a float rounds to the nearest nanosecond, where it truncated.

  • dst_offset() was zero in some historical periods, and next_transition()/prev_transition() could stop where nothing changed or skip a transition.

  • A standard library subclass, such as a pandas Timestamp, is read through its standard library fields, with a WheneverWarning about the data this loses.

  • OffsetDateTime.replace(offset=...) no longer emits StaleOffsetWarning.

  • On Python 3.10 and 3.11, the Rust extension’s first time zone lookups from several threads at once could fail for valid IDs.

  • help() on Rust extension methods that take keyword arguments showed no documentation.

  • An Instant pickled by the pure-Python backend of 0.8.0 to 0.10.0 loads correctly in the Rust extension.

  • Adding itemized deltas raised when nanoseconds reached a whole second.

  • The type stubs accept Date + ItemizedDateDelta and sorted() on mixed exact-time types, and reject disambiguation= where it can’t apply.

  • An unknown time zone ID raises TimeZoneNotFoundError wherever it’s given, including inside an ISO string.

  • Instant.parse() accepted a weekday that contradicts the date.

  • RFC 2822 parsing was too lenient in the pure-Python backend. Formatting rounds an offset’s seconds to the nearest minute, as patterns do, where it truncated them, and it no longer writes -0000 (unknown offset) for a negative offset under a minute.

  • With time-machine installed, the Rust extension’s now() was off before 1970 and raised past 2262.

  • In the pure-Python backend, importing a utility such as patch_current_time before the core types no longer raises a circular-import ImportError.

  • A copied or unpickled TimePatch handle could still move the clock after its patch had ended.

  • available_timezones() no longer follows symlinks to directories, as zoneinfo doesn’t.

  • The Rust extension supports subinterpreters with their own GIL on Python 3.12 too, not only on 3.13 and later.

  • In a pattern, '' inside a quoted run is a literal quote, and X parses a lowercase z, as ISO 8601 parsing does.

  • repr(Weekday.MONDAY) is Weekday.MONDAY.

  • Formatting VV without a time zone ID consistently raises, and IsoWeekDate.parse_iso() accepts a lowercase w.

  • format_iso(basic=...) reads its flag by truthiness, like every other flag.

Migration summary. Each spelling in this table warns at runtime. A type checker that implements PEP 702 (@deprecated) also flags each deprecated method, keyword, and value at the call site; TZPATH, DisambiguateStr, CalendarUnitCompositionWarning, and the pattern specifiers warn at runtime only:

Deprecated spelling

Preferred spelling

disambiguate=

disambiguation=

DisambiguateStr

DisambiguationStr

from_timestamp_millis(v)

from_timestamp(v, unit="millisecond")

from_timestamp_nanos(v)

from_timestamp(v, unit="nanosecond")

timestamp_millis()

timestamp(unit="millisecond")

timestamp_nanos()

timestamp(unit="nanosecond")

ZonedDateTime.from_timestamp(v, tz=tz)

Instant.from_timestamp(v).to_tz(tz)

ZonedDateTime.from_timestamp_millis(v, tz=tz)

Instant.from_timestamp(v, unit="millisecond").to_tz(tz)

ZonedDateTime.from_timestamp_nanos(v, tz=tz)

Instant.from_timestamp(v, unit="nanosecond").to_tz(tz)

OffsetDateTime.from_timestamp(v, offset=o)

Instant.from_timestamp(v).to_fixed_offset(o)

OffsetDateTime.from_timestamp_millis(v, offset=o)

Instant.from_timestamp(v, unit="millisecond").to_fixed_offset(o)

OffsetDateTime.from_timestamp_nanos(v, offset=o)

Instant.from_timestamp(v, unit="nanosecond").to_fixed_offset(o)

format_iso(tz="always")

format_iso(tz_id_display="required")

format_iso(tz="auto")

format_iso(tz_id_display="if_available")

format_iso(tz="never")

format_iso(tz_id_display="omit")

to_system_tz()

to_tz(SYSTEM_TZ)

assume_system_tz()

assume_tz(SYSTEM_TZ)

Date.today_in_system_tz()

Date.today(SYSTEM_TZ)

ZonedDateTime.now_in_system_tz()

ZonedDateTime.now(SYSTEM_TZ)

ZonedDateTime.from_system_tz(...)

ZonedDateTime(..., tz=SYSTEM_TZ)

ZonedDateTime.tz

ZonedDateTime.tz_id

delta.add(other, relative_to=r, in_units=u)

delta.add(other, month_composition_ok=True).in_units(u, relative_to=r)

cal_unit_composition_ok=

month_composition_ok=

CalendarUnitCompositionWarning

MonthCompositionWarning

exact_eq()

strict_eq()

parse(..., format=p)

parse(..., pattern=p)

pattern h / hh

H / HH

pattern :SS

[:ss]

pattern :SS.fff

[:ss.fff]

pattern :SS.FFF

[:ss.FFF]

separator-free pattern SS

[ss]

offset=2, replace(offset=2), to_fixed_offset(2), assume_fixed_offset(2), OffsetDateTime.now(2)

hours(2) in place of 2

MonthDay.is_leap()

MonthDay.is_leap_day()

ZonedDateTime.is_ambiguous()

ZonedDateTime.is_repeated()

TZPATH

get_tzpath()

[:ss.fff] isn’t a pure rename of :SS.fff: with zero seconds and fraction, the old spelling emitted a dangling 12:00.000 instead of 12:00.

tz_id is typed str | None, where tz was typed str. Code that passed .tz where a str was required now needs a check or an assertion.

0.10.5 (2026-08-07)

  • Add binary wheels for Python 3.15.

  • Clarified comparison between ceil/expand and floor/trunc rounding modes in docs (Thanks to @LilyFirefly)

0.10.4 (2026-08-02)

Added

  • Support addition and subtraction on itemized deltas without a reference date(time). This operation is performed itemwise and emits a CalendarUnitCompositionWarning when nonzero calendar units are involved, since arithmetic on these units may yield unintuitive results. Operators + and - are now also supported with the same warning behavior.

  • Support + and - operators between date(times) and itemized deltas.

  • Support reflected addition (TimeDelta + datetime) wherever the corresponding datetime + TimeDelta operation is supported.

  • Added WheneverWarning as the base class for all warnings emitted by whenever, allowing package-wide suppression or escalation.

  • Moved the itemized delta implementation to Python. The Rust extension imports it if needed. This reduces the binary size and improves maintainability.

  • Improve warning messages.

Fixed

  • Fixed an overflow when validating extremely large rounding increments in the Rust extension. They now raise ValueError.

0.10.3 (2026-07-17)

  • Fixed the pure Python implementation accepting invalid basic-format times with a separatorless fraction (e.g. 20200101 or 20103000). These now raise ValueError like the Rust extension, instead of an AssertionError or silently parsing a wrong value. Thanks to @gaoflow for the report and fix (#391).

  • Fixed several ISO parser edge cases involving malformed numeric components, empty fractions, 24:00, trailing duration separators, and overflow in the Rust duration parsers.

  • Removed CI checks for PyPy 3.10, which is EOL.

  • Parsing a non-ASCII time zone ID now raises ValueError instead of TimeZoneNotFoundError in the pure Python version, making it consistent with the Rust extension. Thanks to @gaoflow for the report and fix (#393)

0.10.2 (2026-07-06)

  • Fixed an issue in the pure Python implementation where invalid format patterns containing a trimmed-fraction (F) field would raise an AttributeError instead of the intended ValueError. Thanks to @gaoflow for this report and fix (#386).

0.10.1 (2026-07-03)

Improved

  • Reduced import time by ~60% when the Rust extension is active. This was achieved by deferring the import of several internal submodules and time zone database setup until they are actually needed.

Added

  • Added "week_mon" and "week_sun" as valid units for start_of() and end_of() on Date, PlainDateTime, ZonedDateTime, and OffsetDateTime.

Fixed

  • Restored utility functions to whenever.__all__ and ensured dir(whenever) includes lazily loaded public attributes without importing them.

  • Fixed Date.today_in_system_tz() in the Rust extension so it uses whenever’s cached system time zone instead of from datetime.

  • Fixed ZonedDateTime.start_of() and end_of() around DST transitions. Calendar-unit boundaries are exactly 1 nanosecond before the next start_of(). Sub-day units preserve the current occurrence of repeated local times when possible, while correctly handling gaps and folds shorter than the requested unit.

  • Fixed a pure-Python regression introduced in 0.10.0 that could return incorrect offsets for part of a year. This occurred when a time zone switched from explicitly recorded transitions to recurring transition rules partway through that year. This affected a handful of time zones, and in practice only when system time zone data wasn’t present (e.g. on Windows). The Rust extension (the default) was not affected. A rigorous test ensures this won’t regress again in the future.

  • Limit free threading support to Python 3.14 and later. In 3.13, free threading is still experimental and not yet stable.

0.10.0 (2026-04-05)

A big release with several breaking changes and improvements. Highlights are the new delta API, customizable string formatting and parsing, and since()/until() methods for calculating differences between datetimes. See the full list below.

Breaking changes

  • DateTimeDelta and DateDelta have been replaced by ItemizedDelta and ItemizedDateDelta, respectively. The helper functions for creating calendar deltas (years(), months(), weeks(), days()) have also been deprecated.

    The new deltas are fully un-normalized, meaning “90 minutes” and “1 hour and 30 minutes” are distinct values. They implement the Mapping interface and support a rich set of operations including add(), subtract(), total(), in_units(), replace(), and sign().

    Rationale: the “partially” normalized approach was confusing to users. A fully un-normalized approach also better fits the new API for calculating deltas between datetimes. This approach is also more consistent with other libraries, and allows for more control over formatting and parsing of deltas.

    Migration:

    • Replace DateDelta(...) with ItemizedDateDelta(...).

    • Replace DateTimeDelta(...) with ItemizedDelta(...).

    • Replace years(), months(), weeks(), days() helper functions with ItemizedDateDelta(years=...), etc. Or, if passing to a datetime method, use keyword arguments directly (e.g. dt.add(years=1, months=2)).

    • Replace .in_months_days() and .in_months_days_secs_nanos() with .in_units(['months', 'days']) and .in_units(['months', 'days', 'seconds', 'nanoseconds']), respectively.

    • The Date +/- operators with DateDelta are deprecated; use add()/subtract() instead.

    • The Date - operator between two dates is deprecated; use since() or subtract() instead.

  • The ignore_dst parameter (which was used to enable DST-unsafe operations) has been replaced by a warnings mechanism that allows users to suppress or escalate DST-related warnings using per-method keyword arguments or Python’s standard warning filters.

    Rationale: The ignore_dst parameter was a source of confusion, and made the OffsetDateTime APIs less compatible.

    Migration:

    • Replace ignore_dst=True with the appropriate keyword argument:

      • OffsetDateTime methods: stale_offset_ok=True

      • PlainDateTime methods: naive_arithmetic_ok=True

      • TimeDelta methods: days_assumed_24h_ok=True

    • Alternatively, use Python’s warnings.filterwarnings() to suppress StaleOffsetWarning, NaiveArithmeticWarning, or DaysAssumed24HoursWarning.

    • ignore_dst is still accepted (with a deprecation warning) and will be removed in a future release.

  • Behavior of an edge case is changed: disambiguation of non-existent times as a result of calendar arithmetic (or replace()) no longer tries to reuse the previous offset. This change also fixes a rare bug in case a time zone transition skips an entire day (like the Samoa time zone did in 2011) (#252).

    Rationale: Unlike the case of repeated times, reusing the previous offset for non-existent times doesn’t have the advantage of preventing unexpected jumps in time. The new behavior is consistent with other libraries.

  • Dropped Python 3.9 support

    Rationale: Python 3.9 is EOL since October 2025. Python 3.9 only accounts for less than 0.1% of downloads.

  • Removed format_common_iso() and parse_common_iso() methods. Use format_iso() and parse_iso() instead. These have been deprecated since 0.9.0.

  • The round() methods are stricter about keyword-only and positional-only arguments.

Deprecated

  • TimeDelta.in_hours(), .in_minutes(), .in_seconds(), .in_milliseconds(), .in_microseconds(), .in_nanoseconds(), .in_days_of_24h(), and .in_hrs_mins_secs_nanos(). Use total() or in_units() instead.

  • Date.days_since() and Date.days_until(). Use since() and until() with total='days' instead.

  • py_date(), py_time(), py_datetime(), and py_timedelta(). Use the new to_stdlib() method instead, which provides a consistent name across all types.

  • from_py_date(), from_py_time(), from_py_datetime(), and from_py_timedelta(). Use the constructor directly instead (e.g. Date(datetime.date(...))).

  • parse_strptime() methods on OffsetDateTime and PlainDateTime. Use the new parse() method instead.

  • ZonedDateTime.start_of_day(). Use start_of("day") instead.

Added or improved

  • Parsing methods now accept leap seconds (second value of 60), normalizing them to 59. This applies to ISO 8601, RFC 2822, and custom format strings.

  • New ZonedDateTime.next_transition() and ZonedDateTime.prev_transition() methods for finding the next or previous UTC offset transition (e.g. DST change) relative to the current datetime. Returns None for time zones without transitions (e.g. UTC or fixed-offset).

  • New since() and until() methods on Date, ZonedDateTime, OffsetDateTime, and PlainDateTime for calculating the difference between two values in terms of specific calendar/time units.

  • New format() and parse() methods on Date, Time, PlainDateTime, OffsetDateTime, ZonedDateTime, and Instant for custom format/parse patterns. Example: Date(2024, 3, 15).format("YYYY/MM/DD") → "2024/03/15". These types also support __format__, enabling f-string usage: f"{date:YYYY/MM/DD}". See the pattern format documentation for details.

  • New TimeDelta.total() and TimeDelta.in_units() methods for converting a time delta into specific units.

  • New TimeDelta.add() and TimeDelta.subtract() methods. The operators + and - were supported already, but these methods make it easier for simple operations, as well as making the API more consistent with other classes.

  • New OffsetDateTime.assume_tz() method for associating an offset datetime with a time zone.

  • round() methods now support four new rounding modes: trunc, expand, half_trunc, and half_expand. They also now support larger and irregular values for increment. TimeDelta.round() now supports days and weeks as rounding units (with a warning about 24-hour days).

  • All types that have a Python standard library equivalent now also accept these objects in the constructor. For example: Date(datetime.date(2024, 1, 1)).

  • New ZonedDateTime.dst_offset() and ZonedDateTime.tz_abbrev() methods for querying time zone metadata (DST offset adjustment and time zone abbreviation).

  • Warning classes (StaleOffsetWarning, NaiveArithmeticWarning, DaysAssumed24HoursWarning) and corresponding per-method keyword arguments (stale_offset_ok, naive_arithmetic_ok, days_assumed_24h_ok) for fine-grained control over DST-related warnings.

  • New IsoWeekDate type for representing ISO calendar week dates. Construct with IsoWeekDate(year, week, weekday) or parse with IsoWeekDate("2024-W01-1"). Convert from Date with Date.iso_week_date().

  • New calendar methods on Date: day_of_year(), days_in_month(), days_in_year(), in_leap_year(), next_day(), prev_day(), nth_weekday_of_month(), nth_weekday(), start_of(), end_of().

  • New calendar methods on PlainDateTime, ZonedDateTime, OffsetDateTime: day_of_year(), days_in_month(), days_in_year(), in_leap_year(), start_of(), end_of().

  • New calendar methods on YearMonth: days_in_month(), days_in_year(), in_leap_year().

  • A huge revamp and expansion of the documentation. The structure and navigability of API reference and overview pages has been improved. Several new pages have been added, including:

    • An explanation of the fundamental concepts of time

    • An overview of Python’s datetime pitfalls

    • Explanation of the rounding API

  • YearMonth, MonthDay, Weekday, and IsoWeekDate are now implemented in pure Python always, reducing the compiled extension size.

  • Instant.add/subtract now support passing TimeDelta instances. Instead of rejecting days and weeks, these methods now emit a warning about DST issues, consistent with the behavior of TimeDelta.

Fixed

  • (Pure-Python version) Fixed incorrect behavior of < operator between Time instances if nanoseconds are involved.

0.9.5 (2026-01-11)

Fix issue where not all windows wheels were built and uploaded (#317)

0.9.4 (2025-12-14)

Added support for free-threaded Python (#166)

0.9.3 (2025-10-16)

Fixed incorrect offsets for some time zones before the start of their first recorded transition (typically pre-1950) (#296)

0.9.2 (2025-09-29)

Methods that take an int, float, or str now also accept subclasses of these types. This is consistent with the behavior of the standard library and improves compatibility with libraries like numpy and pandas (#260)

0.9.1 (2025-09-28)

Added ZonedDateTime.now_in_system_tz() and ZonedDateTime.from_system_tz() as convenience methods to ease migration away from SystemDateTime.

0.9.0 (2025-09-25)

Breaking Changes

  • SystemDateTime has been removed and merged into ZonedDateTime

    To create a more consistent and intuitive API, the SystemDateTime class has been removed. Its functionality is now fully integrated into an enhanced ZonedDateTime, which now serves as the single, canonical class for all datetimes that carry a time zone, including those based on the system’s local time zone.

    Rationale:

    The SystemDateTime class, while useful, created several challenges that compromised the library’s consistency and predictability:

    • Inconsistent Behavior: Methods like replace() and add() on a SystemDateTime instance would use the current system time zone definition, not necessarily the one that was active when the instance was created. This could lead to subtle and unpredictable bugs if the system time zone changed during the program’s execution.

    • API Division: Despite having nearly identical interfaces, SystemDateTime and ZonedDateTime were not interchangeable. A function expecting a ZonedDateTime could not accept a SystemDateTime, forcing users to write more complex code with Union type hints.

    • Maintenance Overhead: Maintaining two parallel APIs for datetimes with a time zone led to significant code duplication and a higher maintenance burden.

    This change unifies the API by integrating system time zone support directly into ZonedDateTime, providing a single, consistent way to handle all datetimes that carry a time zone. The original use cases for SystemDateTime are fully supported by the improved ZonedDateTime.

    This new, unified approach also provides two major benefits:

    • Performance: Operations on a ZonedDateTime representing a system time are now orders of magnitude faster than they were on the old SystemDateTime.

    • Cross-Platform Consistency: The new whenever.reset_system_tz() function provides a reliable, cross-platform way to update the library’s view of the system time zone, replacing the previous reliance on the Unix-only time.tzset().

    Migration:

    • Replace all SystemDateTime with ZonedDateTime in all type hints.

    • Replace SystemDateTime.now() with ZonedDateTime.now_in_system_tz() (whenever >=0.9.1) or Instant.now().to_system_tz() (whenever <0.9.1)

    • Replace SystemDateTime(...) constructor calls with ZonedDateTime.from_system_tz(...) (whenever >=0.9.1) or PlainDateTime(...).assume_system_tz() (whenever <0.9.1)

    • Check calls to .to_system_tz() and .assume_system_tz(): these methods now return a ZonedDateTime instance. In most cases, no code change is needed.

    • Instead of time.tzset(), use whenever.reset_system_tz() to update the system time zone (for whenever only).

  • ZonedDateTime instances with a system time zone may in rare cases not have a known IANA time zone ID (the tz property will be None). This is an unfortunate limitation of some platforms. Such ZonedDateTime instances can still be used for all operations, and will account for DST correctly. However, these instances cannot be pickled, and their ISO format will not be able to include the time zone ID.

    Rationale: This is an necessary compromise for broad system time zone support. Other libraries (and Python’s own zoneinfo) have similar limitations.

  • The repr() of all classes now includes quotes: e.g. Date("2023-10-05"). Since all constructors now also accept ISO 8601 strings, the repr() output can be directly used as input and thus eval(repr(obj)) == obj.

    Rationale: This makes the types easier to use in interactive sessions and tests. A round-trippable repr() is also a common expectation for primitive types.

  • Renamed [format|parse]_common_iso methods to [format|parse]_iso. The old methods are still available (but deprecated) to ease the transition.

    Rationale: The “common” qualifier is no longer necessary because these methods have been expanded to handle a wider range of ISO 8601 formats.

  • Removed the deprecated local() methods (use to_plain() instead).

  • Removed the deprecated instant() method (use to_instant() instead).

Improved

  • All classes can now be directly instantiated from an ISO 8601 formatted string passed as a sole argument. For example, Date("2023-10-05") is equivalent to Date(2023, 10, 5) (which is still supported, of course).

  • Customizable ISO 8601 Formatting: The format_iso() methods now accept parameters to customize the output. You can control the separator (e.g., 'T' or ' '), the smallest unit (from hour to nanosecond), and toggle the “basic” (compact) or “extended” format.

    Also, the formatting is now significantly faster. Up to 5x faster for ZonedDateTime, which is now 10x faster than the standard library’s datetime.isoformat().

Fixed

  • Resolved a memory leak in the Rust extension where time zone objects that were no longer in use were not properly evicted from the cache.

  • Fixed a rare bug in determining the UTC offset for times far in the future

  • Fixed PlainDateTime constructor raising TypeError instead of ValueError when passed invalid parameters.

  • TZ IDs starting with a ./ are now properly rejected. Other path traversal attempts were already handled correctly.

  • More robust time zone refcounting in the Rust extension, preventing crashes in rare cases (#270)

  • Panics in Rust extension no longer crash the interpreter, raise RuntimeError instead

0.8.9 (2025-09-21)

  • Fixed not all test files included in source distribution (#266)

  • Uploaded missing Python 3.14 wheels

0.8.8 (2025-07-24)

  • Add wheels for Python 3.14 now that its ABI is stable.

  • Add a pure Python wheel so platforms without binary wheels can use whenever’s pure Python version without having to go through the source build process (#256)

0.8.7 (2025-07-18)

  • Fix some MIN and MAX constants not documented in the API reference.

  • Add Time.MIN alias for Time.MIDNIGHT for consistency (#245)

  • Fix bug in rounding of midnight ZonedDateTime values in “ceil”/day mode (#249)

0.8.6 (2025-06-23)

  • Improve error message of ZonedDateTime.from_py_datetime() in case the datetime’s ZoneInfo.key is None.

  • Fix performance regression in Date.day_of_week() (#244)

0.8.5 (2025-06-09)

  • Relax build requirements. It now only depends on setuptools_rust if opting to build the Rust extension (#240)

  • Fixed not all Rust files included in source distribution.

  • Update some outdated docstrings.

0.8.4 (2025-05-28)

  • Fix Pydantic JSON schema generation in certain contexts, which affected FastAPI doc generation.

0.8.3 (2025-05-22)

  • Ensure Pydantic parsing failures of whenever types always result in a proper ValidationError, not a TypeError.

0.8.2 (2025-05-21)

  • Allow Pydantic to generate JSON schema for whenever types. This is particularly useful for generating OpenAPI schemas for FastAPI.

0.8.1 (2025-05-21)

New

  • Added support for Pydantic serialization/deserialization of whenever types in the ISO 8601 format. This functionality is in preview, and may be subject to change in the future. (#175)

Fixed

  • Weekday enum values from the Rust extension are now pickleable.

  • Solve crash if Python’s garbage collection occurs while the Rust extension is still initializing.

  • Fixed a crash in parsing malformed fractional TimeDelta seconds (#234)

Improved

  • Time.from_py() now ignores any tzinfo, instead of raising an error.

  • A comprehensive refactor of the Rust extension module eliminates most unnecessary unsafe code, making it safer and more idiomatic.

0.8.0 (2025-05-01)

A big release with several improvements and breaking changes that lay the groundwork for the eventual 1.0 release.

Improved

  • Time zone operations in the Rust extension are now a lot faster (5-8x), due to a new implementation replacing the use of the standard library zoneinfo module. (#202)

  • The parse_common_iso() methods support a wider range of ISO 8601 formats. See the updated documentation for details. (#204)

  • Added an “examples” page to the documentation with practical snippets. (#198)

  • RFC2822 parsing is now more robust and faster. (#200)

  • Import speed is improved significantly for both the Rust and pure Python versions (#228)

Breaking changes

  • LocalDateTime has been renamed to PlainDateTime, and the local() method has been renamed to to_plain(). The old names are still available (but deprecated) to ease the transition.

    Rationale: In observing adoption of the library, the term “local” causes confusion for a number of users, since the term “local” is so overloaded in the Python world. PlainDateTime is used in Javascript’s Temporal API, and seems to resonate better with users. See the FAQ for a detailed discussion on the name.

  • Rename instant() method to to_instant()

    Rationale: The new name is more consistent with the rest of the API.

  • Removed the [format|parse]_rfc3339 method.

    Rationale: The improved ISO 8601 parsing method is now RFC 3339 compatible, making this method unnecessary. Strict RFC 3339 parsing can still be done with strptime, if desired

  • Passing invalid time zone names now raise a whenever.TimeZoneNotFoundError (subclass of ValueError) instead of zoneinfo.ZoneInfoNotFoundError (subclass of KeyError).

    Rationale: This ensures whenever is independent of the zoneinfo module, and its particularities don’t leak into the whenever API.

  • TimeDelta.from_py_timedelta no longer accepts timedelta subclasses.

    Rationale: timedelta subclasses (like pendulum.Duration) often add other time components, which cannot be guaranteed to be handled correctly.

  • The strptime methods have been renamed parse_strptime, and its format argument is now a keyword-only argument.

    Rationale: This ensures all parsing methods have the parse_ prefix, helping in API consistency and discoverability. The keyword-only argument helps distinguish between the format string and the string to parse.

  • The InvalidOffset exception has been renamed InvalidOffsetError

    Rationale: this more clearly indicates that this is an error condition. See #154 for discussion.

  • SkippedTime and RepeatedTime are now subclasses of ValueError.

    Rationale: it ensures these exceptions can be caught together with other exceptions like InvalidOffsetError and TimeZoneNotFoundError during parsing.

  • Whenever is no longer affected by ZoneInfo.clear_cache() or zoneinfo.reset_tzpath(), since it now uses its own cache with corresponding methods.

    Rationale: This ensures whenever is independent of zoneinfo in both Rust and pure Python implementations.

Fixed

  • Improved robustness of date calculations at extreme boundaries. (#219)

  • Fixed a bug in the pure-Python version of ZonedDateTime.exact_eq() that could cause false positives in some cases.

  • Fixed incorrect type stubs for day_length() and start_of_day() methods.

  • Corrected the description of parameters accepted by now(). (#213)

0.7.3 (2025-03-19)

  • Fixed type annotations of Weekday enum values, so they are properly marked as int.

0.7.2 (2025-02-25)

  • Fixed round() method behaving incorrectly when increment argument is not passed explicitly (#209)

0.7.1 (2025-02-24)

  • Date.add and Date.subtract now support DateDelta to be passed as sole positional argument. This is consistent with the behavior of datetime classes.

  • Improved performance and robustness of date calculations at extreme boundaries

  • Minor fixes to docstrings

0.7.0 (2025-02-20)

This release adds rounding functionality, along with a small breaking change (see below).

Breaking changes

  • TimeDelta.py_timedelta() now truncates nanoseconds to microseconds instead of rounding them. Use the new round() method to customize rounding behavior.

Added

  • Added round() to all datetime, Instant, and TimeDelta classes

  • Add floor division and modulo operators to TimeDelta

  • Add is_ambiguous(), day_length() and start_of_day() to SystemDateTime, for consistency with ZonedDateTime.

  • Improvements to documentation

0.6.17 (2025-01-30)

  • Added day_length() and start_of_day() methods to ZonedDateTime to make it easier to work with edge cases around DST transitions, and prepare for implementing rounding methods in the future.

  • Fix cases in type stubs where positional-only arguments weren’t marked as such

0.6.16 (2024-12-22)

  • Fix bug in ZonedDateTime repr() that would mangle some time zone names

  • Make disambiguate argument optional, defaulting to "compatible".

    Rationale: This required parameter was a frequent source of irritation for users. Although “explicit is better than implicit”, other modern libraries and standards also choose an (implicit) default. For those that do want to enforce explicit handling of ambiguous times, a special stubs file or other plugin may be introduced in the future.

  • Various small fixes to the docs

0.6.15 (2024-12-11)

  • Add Date.days_[since|until] methods for calculating the difference between two dates in days only (no months or years)

  • Improve docs about arithmetic rules for calendar and time units.

0.6.14 (2024-11-27)

  • Ensure docstrings and error messages are consistent in Rust extension as well as the pure-Python version

  • Remove undocumented properties hour/minute/etc from Instant that were accidentally left in the Rust extension.

  • exact_eq() now also raises TypeError in the pure Python version when comparing different types.

0.6.13 (2024-11-17)

Added

  • Make from_py_datetime() on Instant/OffsetDateTime less pedantic. They now accept any aware datetime

  • New Date.today_in_system_tz() convenience method

Fixed

  • Parsing UTC offsets with out-of-range minute components (e.g. 06:79) now raises the expected parsing failure.

  • Note in parse_rfc2822() docstring that it doesn’t (yet) validate the input, due to limitations in the underlying parser.

0.6.12 (2024-11-08)

  • Fixed format_rfc3339() docstrings that incorrectly included a T separator. Clarified that T can be added by using the format_common_iso() method instead. (#185)

0.6.11 (2024-11-04)

Added

  • Added YearMonth and MonthDay classes for working with year-month and month-day pairs

Fixed

  • whenever.__version__ is now also accessible when Rust extension is used

0.6.10 (2024-10-30)

Improved

  • Improve method documentation and autocomplete support (#172, #173, #176)

Fixed

  • Remove lingering undocumented offset on Instant

  • Fix incorrect LocalDateTime.difference return type annotation

0.6.9 (2024-09-12)

  • Clarify DST-related error messages (#169)

0.6.8 (2024-09-05)

  • Fix object deallocation bug that caused a crash in rare cases (#167)

0.6.7 (2024-08-06)

  • Add Python 3.13 binary wheels, now that its ABI is stable

  • Small improvements to import speed

0.6.6 (2024-07-27)

  • Fix potential memory leak in .now() if time-machine is used

0.6.5 (2024-07-27)

  • from_timestamp now also accepts floats, to ease porting code from datetime (#159)

  • Fixed incorrect fractional seconds when parsing negative values in from_timestamp methods.

  • Fix some places where ValueError was raised instead of TypeError

0.6.4 (2024-07-26)

  • Add helper patch_current_time for patching current time in whenever (only) (#147)

  • Support patching the current time with time-machine (#147)

  • Remove undocumented year/month/day/offset properties from Instant

  • Reduce size of binary distributions

  • Clarify contribution guidelines

0.6.3 (2024-07-13)

  • Improve robustness and speed of keyword argument parsing in Rust extension (#149)

  • Add more answers to common questions in the docs and FAQ (#148, #150)

0.6.2 (2024-07-04)

  • Add third-party licenses to distributions

0.6.1 (2024-07-04)

  • Small updates to project metadata

0.6.0 (2024-07-04)

A big release touting a Rust extension module and an API more consistent with other modern libraries.

Added or improved

  • Implement as a Rust extension module, leading to a big speedup

  • Add replace_date and replace_time methods to datetimes.

  • Add Date.MIN and Date.MAX constants.

  • from_py_* methods are more robust.

  • The pickle format for most types is now more efficient.

Breaking changes

  • UTCDateTime is now Instant. Removed methods that were specific to UTC.

    Rationale: Instant is simpler and more conceptually clear. It also avoids the mistake of performing calendar arithmetic in UTC.

  • NaiveDateTime is now LocalDateTime

    Rationale: “Local” is more descriptive for describing the concept of “wall clock” time observed locally by humans. It’s also consistent with other libraries and standards.

  • Nanosecond precision is now the default for all datetimes and deltas. nanosecond is a keyword-only argument for all constructors, to prevent mistakes porting code from datetime (which uses microseconds).

    Rationale: Nanosecond precision is the standard for modern datetime libraries.

  • Unified [from_]canonical_format methods with [from_]common_iso8601 methods into [format|parse]_common_iso methods.

    Rationale: This cuts down on the number of methods; the performance benefits of separate methods aren’t worth the clutter.

  • Timestamp methods now use integers instead of floats. There are now separate methods for seconds, milliseconds, and nanoseconds.

    Rationale: This prevents loss of precision when converting to floats, and is more in line with other modern libraries.

  • Renamed [from_][rfc3339|rfc2822] methods to [format|parse]_[rfc3339|rfc2822].

    Rationale: Consistency with other methods.

  • Added explicit ignore_dst=True flag to DST-unsafe operations such as shifting an offset datetime.

    Rationale: Previously, DST-unsafe operations were completely disallowed, but to a frustrating degree. This flag is a better alternative than having users resort to workarounds.

  • Renamed as_utc, as_offset, as_zoned, as_local to to_utc, to_fixed_offset, to_tz, to_system_tz, and the NaiveDateTime.assume_* methods accordingly

    Rationale: “to” better clarifies a conversion is being made (not a replacement), and “fixed offset” and “tz” are more descriptive than “offset” and “zoned”.

  • disambiguate= is non-optional for all relevant methods. The only exception is the constructor, which defaults to “raise”.

    Rationale: This makes it explicit how ambiguous and non-existent times are handled.

  • Removed weakref support.

    Rationale: The overhead of weakrefs was too high for such primitive objects, and the use case was not clear.

  • Weekdays are now an enum instead of an integer.

    Rationale: Enums are more descriptive and less error-prone, especially since ISO weekdays start at 1 and Python weekdays at 0.

  • Calendar units in Date[Time]Delta can now only be retrieved together. For example, there is no delta.months or delta.days anymore, delta.in_months_days() should be used in this case.

    Rationale: This safeguards against mistakes like (date1 - date2).days which would only return the days component of the delta, excluding months. Having to call in_months_days() is more explicit that both parts are needed.

  • Units in delta cannot be different signs anymore (after normalization).

    Rationale: The use case for mixed sign deltas (e.g. 2 months and -15 days) is unclear, and having a consistent sign makes it easier to reason about. It also aligns with the most well-known version of the ISO format.

  • Calendar units are normalized, but only in so far as they can be converted strictly. For example, 1 year is always equal to 12 months, but 1 month isn’t equal to a fixed number of days. Refer to the delta docs for more information.

    Rationale: This is more in line with TimeDelta which also normalizes.

  • Renamed AmbiguousTime to RepeatedTime.

    Rationale: The new name is more descriptive for repeated times occurring twice due to DST. It also clarifies the difference between “repeated” times and “ambiguous” times (which can also refer to non-existent times).

  • Dropped Python 3.8 support

    Rationale: Rust extension relies on C API features added in Python 3.9. Python 3.8 will be EOL later this year.

0.5.1 (2024-04-02)

  • Fix LocalSystemDateTime.now() not setting the correct offset (#104)

0.5.0 (2024-03-21)

Breaking changes

  • Fix handling of -0000 offset in RFC2822 format, which was not according to the standard. NaiveDateTime can now no longer be created from this format.

  • DateDelta canonical format now uses P prefix.

Improved

  • Add explicit ISO8601 formatting/parsing methods to datetimes, date, time, and deltas.

  • Add missing Date.from_canonical_format method.

  • Separate docs for deltas and datetimes.

  • NaiveDateTime.assume_offset now also accepts integers as hour offsets.

0.4.0 (2024-03-13)

A big release with the main feature being the addition of date/time deltas. I’ve also tried to bundle as many small breaking changes as possible into this release, to avoid having to do them in the future.

Breaking changes

  • LocalDateTime renamed to LocalSystemDateTime.

    Rationale: The LocalDateTime name is used in other libraries for naive datetimes, and the new name is more explicit.

  • LocalSystemDateTime no longer adjusts automatically to changes in the system time zone. Now, LocalSystemDateTime reflects the system time zone at the moment of instantiation. It can be updated explicitly.

    Rationale: The old behavior was dependent on too many assumptions, and behaved unintuitively in some cases. It also made the class dependent on shared mutable state, which made it hard to reason about.

  • The disambiguate= argument now also determines how non-existent times are handled.

    Rationale: This makes it possible to handle both ambiguous and non-existent times gracefully and in a consistent way. This behavior is also more in line with the RFC5545 standard, and Temporal.

  • from_naive() removed in favor of methods on NaiveDateTime. For example, UTCDateTime.from_naive(n) becomes n.assume_utc().

    Rationale: It’s shorter, and more explicit about assumptions.

  • Renamed ZonedDateTime.disambiguated() to .is_ambiguous().

    Rationale: The new name distinguishes it from the disambiguate= argument, which also affects non-existent times.

  • Replaced .py property with .py_datetime() method.

    Rationale: Although it currently works fine as a property, this may be changed in the future if the library no longer contains a datetime internally.

  • Removed properties that simply delegated to the underlying datetime object: tzinfo, weekday, and fold. date and time now return whenever.Date and whenever.Time objects.

    Rationale: Removing these properties makes it possible to create improved versions. If needed, these properties can be accessed from the underlying datetime object with .py_datetime().

  • Renamed .canonical_str() to .canonical_format().

    Rationale: A more descriptive name.

  • Renamed DoesntExistInZone to SkippedTime, Ambiguous to AmbiguousTime.

    Rationale: The new names are shorter and more consistent.

  • Renamed min and max to MIN and MAX.

    Rationale: Consistency with other uppercase class constants

Improved

  • Added a disambiguation="compatible" option that matches the behavior of other languages and the RFC5545 standard.

  • Shortened the repr() of all types, use space separator instead of T.

  • Added sep="T" or " " option to canonical_format()

  • OffsetDateTime constructor and methods creating offset datetimes now accept integers as hour offsets.

  • Added Date and Time classes for working with dates and times separately.

0.3.4 (2024-02-07)

  • Improved exception messages for ambiguous or non-existent times (#26)

0.3.3 (2024-02-04)

  • Add CPython-maintained tzdata package as Windows dependency (#32)

0.3.2 (2024-02-03)

  • Relax overly strict Python version constraint in package metadata (#33)

0.3.1 (2024-02-01)

  • Fix packaging metadata issue involving README and CHANGELOG being installed in the wrong place (#23)

0.3.0 (2024-01-23)

Breaking changes

  • Change pickle format so that backwards-compatible unpickling is possible in the future.

Added

  • Added strptime() to UTCDateTime, OffsetDateTime and NaiveDateTime.

  • Added rfc2822()/from_rfc2822() to UTCDateTime, OffsetDateTime and NaiveDateTime.

  • Added rfc3339()/from_rfc3339() to UTCDateTime and OffsetDateTime

0.2.1 (2024-01-20)

  • added days() timedelta alias

  • Improvements to README, other docs

0.2.0 (2024-01-10)

Breaking changes

  • Disambiguation of local datetimes is now consistent with zoned datetimes, and is also run on replace().

  • Renamed:

    • from_str → from_canonical_str

    • to_utc/offset/zoned/local → as_utc/offset/zoned/local.

    • ZonedDateTime.zone → ZonedDateTime.tz

Added

  • Support comparison between all aware datetimes

  • support subtraction between all aware datetimes

  • Convenience methods for converting between aware/naive

  • More robust handling of zoned/local edge cases

Docs

  • Cleaned up API reference

  • Added high-level overview

0.1.0 (2023-12-20)

  • Implement OffsetDateTime, ZonedDateTime and LocalDateTime

0.0.4 (2023-11-30)

  • Revert to pure Python implementation, as Rust extension disadvantages outweigh its advantages

  • Implement NaiveDateTime

0.0.3 (2023-11-16)

  • Implement basic UTCDateTime

0.0.2 (2023-11-10)

  • Empty release with Rust extension module

0.0.1

  • Dummy release