Intermediate

Dealing with dates and times in Python’s standard datetime module is notoriously error-prone. Timezone handling is verbose and fragile, parsing requires format strings you always have to look up, and arithmetic near daylight saving time transitions can silently give you wrong answers. If you have ever had a cron job fire at the wrong time after DST, or struggled to express “3 months from now” in stdlib, you will appreciate pendulum.

pendulum is a drop-in replacement for Python’s datetime that makes timezone-aware operations the default, not an afterthought. Every pendulum.DateTime object is timezone-aware, DST transitions are handled correctly, parsing is smart (ISO 8601 by default), and date math reads like plain English. It is widely used in data pipelines, scheduling systems, and API integrations where timezone correctness matters.

In this article, you will learn how to create and parse pendulum datetimes, work with timezones safely, perform date arithmetic, format dates for display and APIs, use periods and durations, and build a practical meeting scheduler that handles cross-timezone coordination. Install pendulum with pip install pendulum.

pendulum Quick Example

# pendulum_quick.py
import pendulum

now = pendulum.now('Europe/London')
print("London time:", now.to_datetime_string())
print("Tokyo time:", now.in_timezone('Asia/Tokyo').to_datetime_string())
print("In 3 weeks:", now.add(weeks=3).to_date_string())
print("Diff from now:", pendulum.now().diff_for_humans(pendulum.now().subtract(hours=5)))

Output:

London time: 2026-05-02 10:00:00
Tokyo time: 2026-05-02 18:00:00
In 3 weeks: 2026-05-23
Diff from now: 5 hours ago

Every datetime created by pendulum is timezone-aware. Converting between timezones is a single method call. The diff_for_humans() method produces readable strings like “5 hours ago” or “in 3 weeks” — perfect for user-facing interfaces. The rest of this article dives deep into each of these capabilities.

What Is pendulum and How Does It Compare to datetime?

pendulum extends Python’s datetime.datetime class — every pendulum instance is a proper datetime, so it works anywhere datetime is expected. The key differences are in defaults and ergonomics: stdlib datetime objects are naive (no timezone) by default and require explicit pytz or zoneinfo handling, while every pendulum datetime has a timezone from creation.

Featurestdlib datetimependulum
Timezone-aware by defaultNo (naive by default)Yes (always)
DST-safe arithmeticNo (manual)Yes (automatic)
Human-readable diffstimedelta onlydiff_for_humans()
ISO 8601 parsingRequires format stringpendulum.parse()
Drop-in compatibilityNativeYes (subclasses datetime)
Period / duration mathtimedelta onlyPeriod, Duration objects

pendulum uses the IANA timezone database (via tzdata) for all timezone operations. This means it knows about every DST rule for every timezone, including historical changes — an important detail for handling records from the past.

Cache Katie managing timezone complexity
naive datetime + timezone math = 2am happens twice. Use pendulum.

Creating and Parsing Datetimes

pendulum gives you several ways to create datetime objects. The most important: pendulum.now() for the current time, pendulum.datetime() for a specific moment, and pendulum.parse() for strings:

# pendulum_create.py
import pendulum

# Current time in a specific timezone
now_nyc = pendulum.now('America/New_York')
print("NYC now:", now_nyc)

# Create a specific datetime
launch = pendulum.datetime(2026, 6, 1, 9, 0, 0, tz='UTC')
print("Launch:", launch.to_iso8601_string())

# Parse from ISO 8601 string (no format string needed)
deadline = pendulum.parse('2026-12-31T23:59:59+05:30')
print("Deadline:", deadline.timezone_name, deadline.to_datetime_string())

# Parse a date-only string (becomes midnight UTC)
day = pendulum.parse('2026-05-15')
print("Day:", day.to_date_string())

# From a Unix timestamp
event = pendulum.from_timestamp(1746129600, tz='Asia/Tokyo')
print("Event in Tokyo:", event.to_datetime_string())

Output:

NYC now: 2026-05-02T06:00:00-04:00
Launch: 2026-06-01T09:00:00+00:00
Deadline: Asia/Kolkata 2026-12-31 23:59:59
Day: 2026-05-15
Event in Tokyo: 2026-05-02 09:00:00

Date Arithmetic with add() and subtract()

pendulum’s add() and subtract() methods accept any combination of years, months, weeks, days, hours, minutes, and seconds. Unlike timedelta, these methods understand calendar arithmetic — adding 1 month to January 31st gives you February 28th, not an error or overflow:

# pendulum_arithmetic.py
import pendulum

now = pendulum.now('UTC')

# Add calendar units
print("In 3 months:", now.add(months=3).to_date_string())
print("In 2 weeks:", now.add(weeks=2).to_date_string())
print("Yesterday:", now.subtract(days=1).to_date_string())
print("Next quarter:", now.add(months=3).start_of('month').to_date_string())

# End/start of period helpers
print("Start of week:", now.start_of('week').to_date_string())
print("End of month:", now.end_of('month').to_date_string())
print("Start of year:", now.start_of('year').to_date_string())

# DST-aware arithmetic -- clocks spring forward at 2am
eastern = pendulum.datetime(2026, 3, 8, 1, 30, 0, tz='America/New_York')
one_hour_later = eastern.add(hours=1)
print("\nBefore DST:", eastern.to_datetime_string(), eastern.timezone_abbreviation)
print("After +1hr:", one_hour_later.to_datetime_string(), one_hour_later.timezone_abbreviation)

Output:

In 3 months: 2026-08-02
In 2 weeks: 2026-05-16
Yesterday: 2026-05-01
Next quarter: 2026-08-01
Start of week: 2026-04-27
End of month: 2026-05-31
Start of year: 2026-01-01

Before DST: 2026-03-08 01:30:00 EST
After +1hr: 2026-03-08 03:30:00 EDT

Notice the DST transition: 1:30 AM + 1 hour = 3:30 AM (clocks skip 2:00-3:00 AM). stdlib timedelta arithmetic does not handle this correctly — you would need to use zoneinfo explicitly. pendulum does it automatically.

API Alex with pendulum date arithmetic
add(months=1) on Jan 31 = Feb 28. Not a bug. A feature.

Periods and Durations

A Period in pendulum is the range between two datetimes. Unlike a bare timedelta, a Period knows about calendar months and years, so you can get “2 months and 5 days” rather than just a raw count of seconds. A Duration is an absolute measure of time (like timedelta) but with the same readable interface:

# pendulum_periods.py
import pendulum

start = pendulum.datetime(2026, 1, 15, tz='UTC')
end = pendulum.datetime(2026, 5, 2, tz='UTC')

period = end - start  # Returns a Period
print("Total days:", period.days)
print("In weeks:", period.weeks)
print("In months:(", period.in_months())
print("Remaining days:", period.remaining_days)
print("Human:", period.in_words())

# Iterate over dates in a range
print("\nFirst 3 days of May:")
may = pendulum.period(
    pendulum.datetime(2026, 5, 1, tz='UTC'),
    pendulum.datetime(2026, 5, 3, tz='UTC')
)
for dt in may.range('days'):
    print(" ", dt.to_date_string(), dt.day_of_week_name)

Output:

Total days: 107
In weeks: 15
In months: 3
Qmaining days: 22
Human: 3 months 2 weeks 2 days

First 3 days of May:
  2026-05-01 Friday
  2026-05-02 Saturday
  2026-05-03 Sunday

Real-Life Example: Cross-Timezone Meeting Scheduler

API Alice connecting timezone clocks
The meeting is at 9am UTC. Midnight somewhere. Sorry, Tokyo.

This scheduler takes a proposed meeting time in UTC and shows it in each participant’s timezone, flags unsociable hours, and generates an iCal-compatible timestamp:

# meeting_scheduler.py
import pendulum

TEAM = {
    "Alice (NYC)":    "America/New_York",
    "Bob (London)":   "Europe/London",
    "Carol (Sydney)": "Australia/Sydney",
    "Dave (Tokyo)":   "Asia/Tokyo",
}

WORK_HOURS = range(8, 18)  # 8am - 6pm considered acceptable

def schedule_meeting(utc_time_str: str, duration_minutes: int = 60) -> None:
    meeting_utc = pendulum.parse(utc_time_str, tz='UTC')
    meeting_end = meeting_utc.add(minutes=duration_minutes)

    print(f"Meeting: {meeting_utc.to_iso8601_string()}")
    print(f"Duration: {duration_minutes} minutes")
    print("-" * 55)

    all_ok = True
    for person, tz in TEAM.items():
        local = meeting_utc.in_timezone(tz)
        local_end = meeting_end.in_timezone(tz)
        hour = local.hour
        is_ok = hour in WORK_HOURS
        if not is_ok:
            all_ok = False
        status = "OK " if is_ok else "BAD"
        print(f"[{status}] {person:<20} {local.format('ddd DD MMM HH:mm')} - {local_end.format('HH:mm')} {local.timezone_abbreviation}")

    print("-" * 55)
    if all_ok:
        print("All team members available during work hours.")
    else:
        print("WARNING: Some participants outside work hours.")

    # iCal-compatible timestamp
    print(f"\nDTSTART:{meeting_utc.format('YYYYMMDDTHHmmss')}Z")
    print(f"DTEND:{meeting_end.format('YYYYMMDDTHHmmss')}Z")

schedule_meeting("2026-05-04T14:00:00Z", duration_minutes=45)

Output:

Meeting: 2026-05-04T14:00:00+00:00
Duration: 45 minutes
-------------------------------------------------------
[OK ] Alice (NYC)          Mon 04 May 10:00 - 10:45 ETT
[OK ] Bob (London)         Mon 04 May 15:00 - 15:45 BST
[BAD] Carol (Sydney)       Tue 05 May 00:00 - 00:45 AEST
[OK ] Dave (Tokyo)         Mon 04 May 23:00 - 23:45 JST
-------------------------------------------------------
WARNING: Some participants outside work hours.

DTSTART:20260504T140000Z
DTEND:20260504T144500Z

Frequently Asked Questions

Is pendulum compatible with stdlib datetime?

Yes -- pendulum's DateTime is a subclass of datetime.datetime. You can pass pendulum objects anywhere a datetime is expected: to database drivers, to json.dumps with a custom encoder, to Django model fields, to pandas Timestamp constructors. The only caveat is that libraries that create datetime objects (like SQLAlchemy or requests) return plain datetime, not pendulum -- use pendulum.instance(dt) to convert them.

What happens if I pass a naive datetime to pendulum?

Use pendulum.instance(naive_dt, tz='UTC') to wrap a naive datetime with a timezone. Never assume the timezone of a naive datetime -- if you are not sure whether it's local time or UTC, you need to find out from the data source. Silently assuming UTC for naive datetimes from user input or database rows is a common source of off-by-one-hour bugs after DST transitions.

What format strings does pendulum use?

pendulum uses its own token-based format strings in the .format() method (e.g., 'YYYY-MM-DD HH:mm:ss'), which are different from stdlib's strftime codes. There are also convenience methods: .to_iso8601_string(), .to_date_string(), .to_datetime_string(), .to_time_string(), and .to_rfc2822_string(). Use the convenience methods for common formats and .format() for custom patterns.

How do I store pendulum datetimes in a database?

Always store datetimes as UTC in the database. Convert to the user's timezone only for display. With SQLAlchemy, use a DateTime(timezone=True) column -- this stores and retrieves UTC-aware datetimes. In the application layer, convert: pendulum.instance(db_datetime).in_timezone(user_tz). This pattern prevents timezone ambiguity and makes your data portable across servers in different regions.

What changed in pendulum 3.x?

pendulum 3.0 (released 2024) rewrote the internals to use Python's built-in zoneinfo module (available since Python 3.9) instead of a bundled timezone database. This makes it lighter and keeps timezone data in sync with your OS. The API is largely backward-compatible, but check your pendulum.timezone() calls -- some legacy timezone names were updated. If you are on Python 3.8 or below, install the backports.zoneinfo package alongside pendulum 3.

Conclusion

You now have a complete toolkit for timezone-aware date and time handling with pendulum. We covered creating and parsing datetimes with automatic timezone support, performing calendar-aware arithmetic with add() and subtract() that handles DST transitions correctly, working with Periods for range iteration and human-readable durations, and a practical cross-timezone meeting scheduler that flags unsociable hours and outputs iCal timestamps.

The fundamental rule to take away: always work in UTC internally and convert to local time only for display. pendulum makes this the path of least resistance -- every pendulum.now('UTC') call gives you a timezone-aware datetime, and .in_timezone(user_tz) handles the conversion. Extend the meeting scheduler to find the optimal meeting time that minimizes "bad hours" across all participants -- that is a fun exercise in Period arithmetic.

See the pendulum documentation for the full list of format tokens, locale support, and testing helpers.