(benchmarks)=
(performance)=
# Performance
`whenever` optimizes for three goals that are sometimes in tension:
1. **Runtime speed** — operations should be as fast as possible
2. **Import time** — `import whenever` should feel instant
3. **Package size** — the wheel should stay small for fast/slim installs
These goals can conflict: aggressive inlining improves runtime speed but
increases binary size, which in turn inflates import time.
`whenever` targets a balance, sacrificing a bit of runtime speed in cold
code paths to keep the module compact and fast to import.
```{admonition} Test environment
:class: hint
All benchmarks on this page were run on an Apple M1 Pro (32 GB, macOS 26.5)
using Python 3.14.6 (PGO+LTO build). Import-time measurements use warm
page cache.
```
---
## Runtime speed
`whenever` is compared against Python's standard library (+ `dateutil`),
[Arrow](https://pypi.org/project/arrow/), and [Pendulum](https://pypi.org/project/pendulum/)
across common datetime operations.
*Lower is better. Bars exceeding the axis cutoff are annotated with `>`.*
```{raw} html
```
Why is `whenever` faster?
- **No layering.** Arrow and Pendulum wrap Python's `datetime.datetime` rather
than replacing it. Every operation pays the overhead of crossing extra Python
abstraction layers that `whenever` avoids entirely.
- **Optimised parsing and formatting.** The Rust extension uses hand-written,
single-pass byte-level parsers and formatters: no regex, no intermediate string
objects.
- **Front-loaded computation.** Every `ZonedDateTime` stores its UTC offset at
construction time. Operations like "normalize to UTC" or "subtract two instants"
become simple integer arithmetic with no timezone database lookup at operation
time.
- **Compiled core.** The default wheel is a Rust extension, giving C-level
performance with safe, auditable code. The pure-Python fallback still
benefits from the front-loaded computation model and outperforms Arrow on most
simple operations.
```{admonition} What about the pure-Python version of whenever?
:class: hint
For simple operations — `now()`, ISO parsing,
UTC normalization — it is noticeably faster than Arrow and Pendulum. For
timezone-heavy operations such as `ZonedDateTime` construction or timezone
conversion it is slower, as those use pure-Python timezone code instead
of the C-optimized `zoneinfo` module.
Overall it is in the same ballpark as Arrow and Pendulum.
```
---
## Import time
`import whenever` is nearly free because the package defers all heavy
work until the first attribute access.
`import datetime` is faster on standard CPython builds because `datetime.py`
is a thin wrapper around the built-in C module `_datetime`, which imports
without loading a separate shared library. Third-party packages cannot match
that.
*Other libraries shown for context.*
```{raw} html
```
Import time is mainly kept low through lazy loading of submodules and dependencies:
- The `__init__.py` uses module-level `__getattr__` (PEP 562) to defer the
extension load until first access.
Code that imports `whenever` but doesn't use it pays essentially nothing.
- Dependencies (`datetime`, `zoneinfo`, `pydantic`, `typing`)
are imported on first use, not at module load.
---
## Package size
The chart below compares wheel sizes of `whenever` against other
datetime libraries, as well as some unrelated libraries for context.
A pure-Python wheel is also available for environments where install size
or platform coverage matters more than runtime speed.
```{raw} html
```
`whenever`'s focus on runtime speed and rich API means it is relatively large.
However, it keeps the wheel size reasonable through careful design choices:
- Several types (`Weekday`, `YearMonth`, `MonthDay`, `IsoWeekDate`) are
implemented only in Python even when the extension is active, keeping the
native binary focused on the performance-critical datetime types.
- Inlining is used judiciously: hot code paths are optimized, while cold paths
are prevented from inflating the binary size.
Trade-offs not taken:
- Full, descriptive docstrings (almost 100 KB) are included in the wheel.
- Panics in Rust code are caught and converted to Python exceptions, which
requires unwinding tables and increases binary size. `orjson` compiles
with `panic = "abort"` to avoid this overhead, but `whenever` prioritizes
safety and debuggability over minimal size.
- Rust extensions have more overhead per method than C extensions,
but this is worth the safety and maintainability benefits.
---
## Running the benchmarks yourself
See `benchmarks/comparison/README.md` for setup instructions.
```shell
make bench-compare # full comparison run
make bench-compare-fast # quick comparison run
make bench-compare-docs # full run and update these charts
```