pendulum review
Pendulum 3.2.0 is a timezone-aware date and time layer built on Python's standard `datetime` and `zoneinfo`. Its `DateTime` subclass adds named-zone conversion, calendar arithmetic, period boundaries, parsing, localized relative text, intervals, and durations with month or year units. Version 3.2.0 adds Python 3.14 support, removes the pytz dependency, lazy-loads locales, fixes timezone handling in `parse('now')`, and repairs Rust offset calculations plus interval and duration edge cases. Our installed wheel included py.typed and compiled extensions.
Pendulum 3.2.0 installed in 0.3 seconds and used 5 MB across four packages in our sandbox, with typed APIs, native extensions, and no audit findings. It pays for itself in calendar-heavy scheduling; plain `datetime` and `zoneinfo` remain the better default for services that only move aware timestamps around.
We installed it
| Install | ✓ · 0.3s | 4 packages on disk · 5 MB |
| Import | ✓ | import pendulum in 0.19s · compiled extensions · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pendulum install cleanly?
Yes. In a fresh container with an empty cache, pip install pendulum finished in 0.3s, leaving 4 packages and 5 MB on disk. pip-audit reported no known vulnerabilities.
What does pendulum need to run?
Python >=3.9, and a platform wheel with compiled extensions. In our run import pendulum succeeded in 0.19s, and the package ships py.typed for type checkers.
pendulum or whenever: which should you use?
whenever: Use it when separate types for instants, local datetimes, and zoned datetimes should catch category mistakes. Pendulum 3.2.0 installed in 0.3 seconds and used 5 MB across four packages in our sandbox, with typed APIs, native extensions, and no audit findings.
When should you not use pendulum?
The job only stores UTC instants and performs basic timezone conversion. Standard datetime with zoneinfo has no extra package cost.
Use it if
- Scheduling rules repeatedly add calendar months or days in a named timezone and must survive daylight-saving transitions.
- The service wants aware datetimes by default plus concise conversion, boundary, interval, and humanized-difference methods.
- Localized date output and relative phrases should be generated in Python rather than in every client.
- Existing APIs accept datetime subclasses and your database drivers have been tested with Pendulum values.
- The job only stores UTC instants and performs basic timezone conversion. Standard `datetime` with `zoneinfo` has no extra package cost.
- A database adapter dispatches on exact type. Pendulum's README calls out sqlite3, mysqlclient, and PyMySQL and supplies custom adapters for them.
- Static types should prevent mixing an instant, a local wall time, and a zoned datetime. `whenever` represents those concepts as separate classes.
- The deployment accepts only pure-Python wheels or targets an unusual platform. Our 3.2.0 installation contained compiled `.so` extensions.
- Production remains on Python 3.8. The published 3.2.0 metadata requires Python 3.9 or later.
Setup reality
We installed Pendulum 3.2.0 in a fresh Python 3.12 Bookworm sandbox. The install finished in 0.3 seconds and left four packages occupying 5 MB. Pendulum declares three direct dependencies, requires Python 3.9 or later, and includes py.typed plus compiled .so extensions. import pendulum succeeded in 0.19 seconds. pip-audit found zero known vulnerabilities, and the package reports the MIT License.
No credential or config file is needed. The package uses zoneinfo and installs timezone data support; 3.2.0 no longer depends on pytz. Its release metadata permits Python 3.9, while the current repository README says Python 3.10 and newer for ongoing development. Treat the wheel metadata as the floor for this exact release, then recheck before an upgrade. Time-travel testing helpers require the optional test extra.
DateTime subclasses standard datetime, but some database libraries compare exact types. The project documents adapter registration for sqlite3, mysqlclient, and PyMySQL, and notes a Django/MySQL issue caused by always-aware ISO output. Register adapters before creating connections and round-trip a real column in tests. Locale defaults are process-wide, so pass the locale to formatting calls in a concurrent server instead of changing global state per request.
DST arithmetic needs an explicit rule. Adding one day preserves a calendar intent, while 24 hours preserves elapsed time and may land on a different local clock hour. Pendulum normalizes a nonexistent spring-forward time unless raise_on_unknown_times=True; an autumn duplicate uses fold to select the occurrence. Version 3.2.0 also fixes parse('now', tz=...), invalid intervals, Duration deepcopy with weeks, and an incorrect offset calculation in the Rust extension.
Patterns
Construct and convert aware datetimes create-aware-datetime
import pendulum
meeting = pendulum.datetime(
2026, 9, 15, 9, 30, tz='Europe/Paris'
)
utc_meeting = meeting.in_timezone('UTC')
local_now = pendulum.now('Asia/Kolkata')Pendulum constructors default to UTC. Name the timezone at application boundaries so local-machine settings cannot change results.
Parse ISO and known custom formats parse-datetime
import pendulum
instant = pendulum.parse('2026-08-26T14:05:00+02:00')
date = pendulum.parse('2026-08-26', tz='America/New_York')
known = pendulum.from_format(
'26 Aug 2026 14:05',
'DD MMM YYYY HH:mm',
tz='UTC',
)Use `from_format` when the input contract is fixed. Non-strict free-form parsing needs an explicit day-first policy for ambiguous numeric dates.
Choose days or elapsed hours calendar-arithmetic
import pendulum
start = pendulum.datetime(2026, 3, 28, 12, 0, tz='Europe/Paris')
next_local_noon = start.add(days=1)
exactly_one_day_later = start.add(hours=24)
next_month = start.add(months=1)At a DST boundary, one calendar day and 24 elapsed hours can produce different local clock times. Match the unit to the business rule.
Reject gaps and select folds dst-gap-fold
import pendulum
strict = pendulum.datetime(
2013, 3, 31, 2, 30,
tz='Europe/Paris',
raise_on_unknown_times=True,
)
first = pendulum.datetime(
2013, 10, 27, 2, 30, tz='Europe/Paris', fold=0
)
second = first.replace(fold=1)The spring value raises because that local time never occurred. `fold` distinguishes the two autumn occurrences of the same wall time.
Render a localized difference relative-text
import pendulum
past = pendulum.now('UTC').subtract(minutes=2)
english = past.diff_for_humans(locale='en')
french = past.diff_for_humans(locale='fr')
absolute = past.diff_for_humans(absolute=True, locale='en')Pass locale per call in request-handling code. `pendulum.set_locale()` changes process-wide state.
Build half-open period bounds period-boundaries
import pendulum
now = pendulum.now('UTC')
month_start = now.start_of('month')
next_month_start = month_start.add(months=1)
rows = fetch_between(month_start, next_month_start)A half-open range avoids relying on `end_of('month')` and its inclusive final microsecond.
Measure and iterate an interval interval-measurement
import pendulum
start = pendulum.datetime(2026, 1, 1, tz='UTC')
end = pendulum.datetime(2026, 1, 4, 6, tz='UTC')
span = pendulum.interval(start, end)
print(span.in_hours())
for day in span.range('days'):
process(day)Total methods such as `in_hours()` differ from remainder properties such as `.hours`; choose the one your report means.
Keep months in a duration calendar-duration
import pendulum
term = pendulum.duration(months=1, days=3, hours=4)
renewal = pendulum.datetime(2026, 1, 31, tz='UTC') + term
summary = term.in_words(locale='en')Adding Pendulum Duration preserves calendar units. Converting to standard timedelta collapses variable-length months and years.
Register Pendulum values with sqlite3 sqlite-adapter
import sqlite3
from pendulum import DateTime
sqlite3.register_adapter(
DateTime,
lambda value: value.isoformat(' '),
)
connection = sqlite3.connect('events.db')Register the adapter before opening database connections because sqlite3 dispatches adapters by exact type.
Wrap a standard datetime convert-native-datetime
from datetime import datetime, timezone
import pendulum
native = datetime.now(timezone.utc)
wrapped = pendulum.instance(native)
back_to_native = datetime.fromisoformat(wrapped.isoformat())Check timezone awareness before wrapping external values. A naive datetime has no reliable instant until the application assigns a zone.
Format for machines and people format-output
import pendulum
value = pendulum.datetime(2026, 8, 26, 14, 5, tz='Asia/Kolkata')
machine = value.to_iso8601_string()
human = value.format('dddd, D MMMM YYYY HH:mm', locale='en')Keep the offset in machine output. Localized display strings should not be parsed back as an interchange format.
Freeze Pendulum's clock in a test freeze-time-test
import pendulum
def test_due_date():
frozen = pendulum.datetime(2026, 8, 26, 9, tz='UTC')
with pendulum.travel_to(frozen, freeze=True):
assert pendulum.now('UTC') == frozenInstall `pendulum[test]` first. The 3.x time-travel helpers depend on the optional test extra.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| whenever | PyPI | Use it when separate types for instants, local datetimes, and zoned datetimes should catch category mistakes. |
| arrow | PyPI | Use it for a compact date convenience API after checking whether its timezone and arithmetic rules fit your code. |
| python-dateutil | PyPI | Choose it for recurrence rules, flexible parsing, and standard datetime values without adopting Pendulum's object model. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

