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 newSYSTEM_TZsentinel (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(), andZonedDateTime.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.
TZPATHwas a module attribute that was loaded on access; its replacement,get_tzpath(), is explicit about this.Removed APIs deprecated before 0.11:
DateDelta,DateTimeDelta, theyears(),months(),weeks(), anddays()helpers, legacy standard library conversion methods,TimeDelta.in_*()convenience methods,Date.days_since()andDate.days_until(), deprecatedDateoperators,parse_strptime(),ZonedDateTime.start_of_day(),ignore_dst, andImplicitlyIgnoringDST.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 matchingInstantfactories are deprecated, as are the timestamp factories onOffsetDateTimeandZonedDateTime: construct anInstantfirst, then callto_fixed_offset()orto_tz().Rationale: one unit-selectable API is easier to discover and extend, while
Instantis the natural type for constructing an exact time from a timestamp.Resolving a repeated or skipped local time without an explicit
disambiguation=now emitsImplicitDisambiguationWarning. The"compatible"default still applies, so the result is unchanged, but a warning filter set toerrornow raises. Passdisambiguation="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
TimeDeltaor a factory likehours()instead.Rationale: the unit of a bare integer is implicit, which makes offset values easy to misread or misuse.
Patterns use
H/HHfor the 24-hour clock;h/hhare deprecated. Optional seconds go in brackets aftermm:[:ss],[:ss.fff],[:ss.FFF], or[ss]without a separator. TheSSspellings remain with deprecation warnings through 0.11. Patterns and parsed input strings are ASCII-only.Rationale:
H/HHis 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
ZonedDateTimepreserves its instant under the time zone rules of the loading environment. If those rules give a different offset, the local time follows and aPickleOffsetMismatchWarningis 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
ZonedDateTimefrom a standard librarydatetimeresolves its offset the way ISO parsing does: an offset that disagrees with the time zone rules raisesInvalidOffsetErrorby default, andoffset_mismatch=anddisambiguation=resolve it.Pattern offsets without seconds round an offset with seconds to the nearest minute, as Temporal does. Hour-only
x/Xpatterns reject offsets that still have minutes after rounding.An
ItemizedDeltaand anItemizedDateDeltawith the same components are equal:ItemizedDelta(days=3) == ItemizedDateDelta(days=3).strict_eq()takes only a delta of its own type, and raisesTypeErrorfor the other.Itemized delta
add()andsubtract()are simplified: they compose, and take no reference.relative_to=is deprecated, within_units=,round_mode=,round_increment=,naive_arithmetic_ok=, andstale_offset_ok=; callin_units()on the result instead. The warning is now more precise:CalendarUnitCompositionWarningis nowMonthCompositionWarning, escaped withmonth_composition_ok=, and only years and months trigger it.Rationale:
relative_to=wasin_units()combined withadd(), 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 anItemizedDateDeltawhatever the reference, where a datetime reference gave anItemizedDelta.Two lossy calls now warn:
Date()given adatetime, whose time it drops (call.date()first), andOffsetDateTime.since()/until()where a calendar-unit result depends on the offset held fixed (StaleOffsetWarning).A few meaningless arguments are rejected: a
timewith a tzinfo inTime(), a"week"rounding unit anywhere butTimeDelta.round(), anincrement=alongside aTimeDeltaunit inround(), and pattern fractions followed by a digit field. A non-integerincrement=raisesTypeError, as do bytes given toYearMonth.parse_iso()andIsoWeekDate.parse_iso(), and a.FFFpattern 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/Easternare preserved.patch_current_time()and itsTimePatchhandle, withshift()andmove_to(), are now part of the stable API.Added
offset_mismatch=toZonedDateTimeparsing. A numeric offset is matched at its written precision, whileZalways identifies an exact UTC instant.OffsetDateTime.assume_tz()supports the same policy and usesdisambiguation=when retaining local time.Added
Date.today(tz),YearMonth.add()andsubtract(), andday_of_week()onPlainDateTime,OffsetDateTime, andZonedDateTime.Added millisecond and microsecond totals to datetime differences and
ItemizedDelta.total().ItemizedDeltaandItemizedDateDeltaare hashable, so they can besetmembers anddictkeys.in_units()andtotal()onItemizedDeltaandTimeDeltawith aPlainDateTimeorOffsetDateTimeasrelative_to=accept the escape their warning names:naive_arithmetic_ok=orstale_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-packagein uv’spyproject.toml,--config-settingsinrequirements.txt.WHENEVER_NO_BUILD_RUST_EXTstill works.TimeDelta()acceptstimedeltasubclasses andZonedDateTime()adatetimewith aZoneInfosubclass, like the other standard library overloads.Added the type aliases
DeltaTotalUnitStrandTimestampUnitStr.from whenever import *now includes all the*Straliases.
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(), andsince()/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 calendaradd()/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
TZis a path into a zoneinfo directory, or when/etc/localtimeis a copy of a database file named by/etc/timezone.An empty
TZvariable, or noTZand 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
TimeDeltaby a float rounds to the nearest nanosecond, where it truncated.dst_offset()was zero in some historical periods, andnext_transition()/prev_transition()could stop where nothing changed or skip a transition.A standard library subclass, such as a
pandasTimestamp, is read through its standard library fields, with aWheneverWarningabout the data this loses.OffsetDateTime.replace(offset=...)no longer emitsStaleOffsetWarning.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
Instantpickled by the pure-Python backend of 0.8.0 to 0.10.0 loads correctly in the Rust extension.Adding itemized deltas raised when
nanosecondsreached a whole second.The type stubs accept
Date + ItemizedDateDeltaandsorted()on mixed exact-time types, and rejectdisambiguation=where it can’t apply.An unknown time zone ID raises
TimeZoneNotFoundErrorwherever 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-machineinstalled, the Rust extension’snow()was off before 1970 and raised past 2262.In the pure-Python backend, importing a utility such as
patch_current_timebefore the core types no longer raises a circular-importImportError.A copied or unpickled
TimePatchhandle could still move the clock after its patch had ended.available_timezones()no longer follows symlinks to directories, aszoneinfodoesn’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, andXparses a lowercasez, as ISO 8601 parsing does.repr(Weekday.MONDAY)isWeekday.MONDAY.Formatting
VVwithout a time zone ID consistently raises, andIsoWeekDate.parse_iso()accepts a lowercasew.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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pattern |
|
pattern |
|
pattern |
|
pattern |
|
separator-free pattern |
|
|
|
|
|
|
|
|
|
[: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
CalendarUnitCompositionWarningwhen 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 correspondingdatetime + TimeDeltaoperation is supported.Added
WheneverWarningas 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.
20200101or20103000). These now raiseValueErrorlike the Rust extension, instead of anAssertionErroror 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
ValueErrorinstead ofTimeZoneNotFoundErrorin 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 anAttributeErrorinstead of the intendedValueError. 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 forstart_of()andend_of()onDate,PlainDateTime,ZonedDateTime, andOffsetDateTime.
Fixed
Restored utility functions to
whenever.__all__and ensureddir(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 fromdatetime.Fixed
ZonedDateTime.start_of()andend_of()around DST transitions. Calendar-unit boundaries are exactly 1 nanosecond before the nextstart_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
DateTimeDeltaandDateDeltahave been replaced byItemizedDeltaandItemizedDateDelta, 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
Mappinginterface and support a rich set of operations includingadd(),subtract(),total(),in_units(),replace(), andsign().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(...)withItemizedDateDelta(...).Replace
DateTimeDelta(...)withItemizedDelta(...).Replace
years(),months(),weeks(),days()helper functions withItemizedDateDelta(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 withDateDeltaare deprecated; useadd()/subtract()instead.The
Date-operator between two dates is deprecated; usesince()orsubtract()instead.
The
ignore_dstparameter (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_dstparameter was a source of confusion, and made theOffsetDateTimeAPIs less compatible.Migration:
Replace
ignore_dst=Truewith the appropriate keyword argument:OffsetDateTimemethods:stale_offset_ok=TruePlainDateTimemethods:naive_arithmetic_ok=TrueTimeDeltamethods:days_assumed_24h_ok=True
Alternatively, use Python’s
warnings.filterwarnings()to suppressStaleOffsetWarning,NaiveArithmeticWarning, orDaysAssumed24HoursWarning.ignore_dstis 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()andparse_common_iso()methods. Useformat_iso()andparse_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(). Usetotal()orin_units()instead.Date.days_since()andDate.days_until(). Usesince()anduntil()withtotal='days'instead.py_date(),py_time(),py_datetime(), andpy_timedelta(). Use the newto_stdlib()method instead, which provides a consistent name across all types.from_py_date(),from_py_time(),from_py_datetime(), andfrom_py_timedelta(). Use the constructor directly instead (e.g.Date(datetime.date(...))).parse_strptime()methods onOffsetDateTimeandPlainDateTime. Use the newparse()method instead.ZonedDateTime.start_of_day(). Usestart_of("day")instead.
Added or improved
Parsing methods now accept leap seconds (second value of
60), normalizing them to59. This applies to ISO 8601, RFC 2822, and custom format strings.New
ZonedDateTime.next_transition()andZonedDateTime.prev_transition()methods for finding the next or previous UTC offset transition (e.g. DST change) relative to the current datetime. ReturnsNonefor time zones without transitions (e.g. UTC or fixed-offset).New
since()anduntil()methods onDate,ZonedDateTime,OffsetDateTime, andPlainDateTimefor calculating the difference between two values in terms of specific calendar/time units.New
format()andparse()methods onDate,Time,PlainDateTime,OffsetDateTime,ZonedDateTime, andInstantfor 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()andTimeDelta.in_units()methods for converting a time delta into specific units.New
TimeDelta.add()andTimeDelta.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, andhalf_expand. They also now support larger and irregular values forincrement.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()andZonedDateTime.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
IsoWeekDatetype for representing ISO calendar week dates. Construct withIsoWeekDate(year, week, weekday)or parse withIsoWeekDate("2024-W01-1"). Convert fromDatewithDate.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, andIsoWeekDateare now implemented in pure Python always, reducing the compiled extension size.Instant.add/subtractnow support passingTimeDeltainstances. Instead of rejectingdaysandweeks, these methods now emit a warning about DST issues, consistent with the behavior ofTimeDelta.
Fixed
(Pure-Python version) Fixed incorrect behavior of
<operator betweenTimeinstances 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
SystemDateTimehas been removed and merged intoZonedDateTimeTo create a more consistent and intuitive API, the
SystemDateTimeclass has been removed. Its functionality is now fully integrated into an enhancedZonedDateTime, 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
SystemDateTimeclass, while useful, created several challenges that compromised the library’s consistency and predictability:Inconsistent Behavior: Methods like
replace()andadd()on aSystemDateTimeinstance 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,
SystemDateTimeandZonedDateTimewere not interchangeable. A function expecting aZonedDateTimecould not accept aSystemDateTime, forcing users to write more complex code withUniontype 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 forSystemDateTimeare fully supported by the improvedZonedDateTime.This new, unified approach also provides two major benefits:
Performance: Operations on a
ZonedDateTimerepresenting a system time are now orders of magnitude faster than they were on the oldSystemDateTime.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-onlytime.tzset().
Migration:
Replace all
SystemDateTimewithZonedDateTimein all type hints.Replace
SystemDateTime.now()withZonedDateTime.now_in_system_tz()(whenever >=0.9.1) orInstant.now().to_system_tz()(whenever <0.9.1)Replace
SystemDateTime(...)constructor calls withZonedDateTime.from_system_tz(...)(whenever >=0.9.1) orPlainDateTime(...).assume_system_tz()(whenever <0.9.1)Check calls to
.to_system_tz()and.assume_system_tz(): these methods now return aZonedDateTimeinstance. In most cases, no code change is needed.Instead of
time.tzset(), usewhenever.reset_system_tz()to update the system time zone (forwheneveronly).
ZonedDateTimeinstances with a system time zone may in rare cases not have a known IANA time zone ID (thetzproperty will beNone). This is an unfortunate limitation of some platforms. SuchZonedDateTimeinstances 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, therepr()output can be directly used as input and thuseval(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_isomethods 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 (useto_plain()instead).Removed the deprecated
instant()method (useto_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 toDate(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 (fromhourtonanosecond), 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’sdatetime.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
PlainDateTimeconstructor raisingTypeErrorinstead ofValueErrorwhen 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
RuntimeErrorinstead
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
MINandMAXconstants not documented in the API reference.Add
Time.MINalias forTime.MIDNIGHTfor consistency (#245)Fix bug in rounding of midnight
ZonedDateTimevalues in “ceil”/day mode (#249)
0.8.6 (2025-06-23)¶
Improve error message of
ZonedDateTime.from_py_datetime()in case the datetime’sZoneInfo.keyisNone.Fix performance regression in
Date.day_of_week()(#244)
0.8.5 (2025-06-09)¶
Relax build requirements. It now only depends on
setuptools_rustif 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
whenevertypes always result in a properValidationError, not aTypeError.
0.8.2 (2025-05-21)¶
Allow Pydantic to generate JSON schema for
whenevertypes. This is particularly useful for generating OpenAPI schemas for FastAPI.
0.8.1 (2025-05-21)¶
New
Added support for Pydantic serialization/deserialization of
whenevertypes in the ISO 8601 format. This functionality is in preview, and may be subject to change in the future. (#175)
Fixed
Weekdayenum 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
TimeDeltaseconds (#234)
Improved
Time.from_py()now ignores anytzinfo, instead of raising an error.A comprehensive refactor of the Rust extension module eliminates most unnecessary
unsafecode, 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
zoneinfomodule. (#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
LocalDateTimehas been renamed toPlainDateTime, and thelocal()method has been renamed toto_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.
PlainDateTimeis 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 toto_instant()Rationale: The new name is more consistent with the rest of the API.
Removed the
[format|parse]_rfc3339method.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 desiredPassing invalid time zone names now raise a
whenever.TimeZoneNotFoundError(subclass ofValueError) instead ofzoneinfo.ZoneInfoNotFoundError(subclass ofKeyError).Rationale: This ensures whenever is independent of the
zoneinfomodule, and its particularities don’t leak into thewheneverAPI.TimeDelta.from_py_timedeltano longer acceptstimedeltasubclasses.Rationale: timedelta subclasses (like pendulum.Duration) often add other time components, which cannot be guaranteed to be handled correctly.
The
strptimemethods have been renamedparse_strptime, and itsformatargument 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
InvalidOffsetexception has been renamedInvalidOffsetErrorRationale: this more clearly indicates that this is an error condition. See #154 for discussion.
SkippedTimeandRepeatedTimeare now subclasses ofValueError.Rationale: it ensures these exceptions can be caught together with other exceptions like
InvalidOffsetErrorandTimeZoneNotFoundErrorduring parsing.Whenever is no longer affected by
ZoneInfo.clear_cache()orzoneinfo.reset_tzpath(), since it now uses its own cache with corresponding methods.Rationale: This ensures whenever is independent of
zoneinfoin 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()andstart_of_day()methods.Corrected the description of parameters accepted by
now(). (#213)
0.7.3 (2025-03-19)¶
Fixed type annotations of
Weekdayenum values, so they are properly marked asint.
0.7.2 (2025-02-25)¶
Fixed
round()method behaving incorrectly whenincrementargument is not passed explicitly (#209)
0.7.1 (2025-02-24)¶
Date.addandDate.subtractnow supportDateDeltato 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 newround()method to customize rounding behavior.
Added
Added
round()to all datetime,Instant, andTimeDeltaclassesAdd floor division and modulo operators to
TimeDeltaAdd
is_ambiguous(),day_length()andstart_of_day()toSystemDateTime, for consistency withZonedDateTime.Improvements to documentation
0.6.17 (2025-01-30)¶
Added
day_length()andstart_of_day()methods toZonedDateTimeto 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
ZonedDateTimerepr()that would mangle some time zone namesMake
disambiguateargument 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/etcfromInstantthat were accidentally left in the Rust extension.exact_eq()now also raisesTypeErrorin the pure Python version when comparing different types.
0.6.13 (2024-11-17)¶
Added
Make
from_py_datetime()onInstant/OffsetDateTimeless pedantic. They now accept any aware datetimeNew
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 aTseparator. Clarified thatTcan be added by using theformat_common_iso()method instead. (#185)
0.6.11 (2024-11-04)¶
Added
Added
YearMonthandMonthDayclasses 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
offsetonInstantFix incorrect
LocalDateTime.differencereturn 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()iftime-machineis used
0.6.5 (2024-07-27)¶
from_timestampnow also accepts floats, to ease porting code fromdatetime(#159)Fixed incorrect fractional seconds when parsing negative values in
from_timestampmethods.Fix some places where
ValueErrorwas raised instead ofTypeError
0.6.4 (2024-07-26)¶
Add helper
patch_current_timefor patching current time in whenever (only) (#147)Support patching the current time with time-machine (#147)
Remove undocumented
year/month/day/offsetproperties fromInstantReduce 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_dateandreplace_timemethods to datetimes.Add
Date.MINandDate.MAXconstants.from_py_*methods are more robust.The pickle format for most types is now more efficient.
Breaking changes
UTCDateTimeis nowInstant. Removed methods that were specific to UTC.Rationale:
Instantis simpler and more conceptually clear. It also avoids the mistake of performing calendar arithmetic in UTC.NaiveDateTimeis nowLocalDateTimeRationale: “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.
nanosecondis a keyword-only argument for all constructors, to prevent mistakes porting code fromdatetime(which uses microseconds).Rationale: Nanosecond precision is the standard for modern datetime libraries.
Unified
[from_]canonical_formatmethods with[from_]common_iso8601methods into[format|parse]_common_isomethods.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=Trueflag 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_localtoto_utc,to_fixed_offset,to_tz,to_system_tz, and theNaiveDateTime.assume_*methods accordinglyRationale: “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]Deltacan now only be retrieved together. For example, there is nodelta.monthsordelta.daysanymore,delta.in_months_days()should be used in this case.Rationale: This safeguards against mistakes like
(date1 - date2).dayswhich would only return the days component of the delta, excluding months. Having to callin_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
TimeDeltawhich also normalizes.Renamed
AmbiguousTimetoRepeatedTime.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
-0000offset in RFC2822 format, which was not according to the standard.NaiveDateTimecan now no longer be created from this format.DateDeltacanonical format now usesPprefix.
Improved
Add explicit ISO8601 formatting/parsing methods to datetimes, date, time, and deltas.
Add missing
Date.from_canonical_formatmethod.Separate docs for deltas and datetimes.
NaiveDateTime.assume_offsetnow 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
LocalDateTimerenamed toLocalSystemDateTime.Rationale: The
LocalDateTimename is used in other libraries for naive datetimes, and the new name is more explicit.LocalSystemDateTimeno longer adjusts automatically to changes in the system time zone. Now,LocalSystemDateTimereflects 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 onNaiveDateTime. For example,UTCDateTime.from_naive(n)becomesn.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
.pyproperty 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
datetimeinternally.Removed properties that simply delegated to the underlying
datetimeobject:tzinfo,weekday, andfold.dateandtimenow returnwhenever.Dateandwhenever.Timeobjects.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
DoesntExistInZonetoSkippedTime,AmbiguoustoAmbiguousTime.Rationale: The new names are shorter and more consistent.
Renamed
minandmaxtoMINandMAX.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 ofT.Added
sep="T" or " "option tocanonical_format()OffsetDateTimeconstructor and methods creating offset datetimes now accept integers as hour offsets.Added
DateandTimeclasses 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
tzdatapackage 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()toUTCDateTime,OffsetDateTimeandNaiveDateTime.Added
rfc2822()/from_rfc2822()toUTCDateTime,OffsetDateTimeandNaiveDateTime.Added
rfc3339()/from_rfc3339()toUTCDateTimeandOffsetDateTime
0.2.1 (2024-01-20)¶
added
days()timedelta aliasImprovements 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_strto_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,ZonedDateTimeandLocalDateTime
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