cheat sheet
python-dateutil
Package-level reference for python-dateutil on PyPI — parser, relativedelta, rrule, timezone, install, alternatives.
python-dateutil
What it is
python-dateutil is a long-running extension to the Python standard library datetime module. Its three flagship features are: a permissive date-string parser that accepts almost any human-readable format; relativedelta, an arithmetic object that supports calendar-aware deltas (add 1 month, not "approximately 30.4 days"); and rrule, a full RFC 5545 (iCalendar) recurrence-rule implementation for generating schedules.
Reach for python-dateutil when you need to: parse arbitrary date strings without knowing the format upfront; do calendar-aware arithmetic (month/year addition that handles end-of-month correctly); generate recurring schedules (every second Tuesday); or parse strings that the stdlib datetime.strptime would reject.
Install
pip install python-dateutil
Output: (none — exits 0 on success; depends on six for legacy Python 2/3 compat)
uv add python-dateutil
Output: dependency resolved + added to pyproject.toml
poetry add python-dateutil
Output: updated lockfile + virtualenv install
The PyPI name is python-dateutil; the import name is dateutil. Always use the full PyPI name in install commands.
Versioning & Python support
- Current line is the
2.9.xseries. Releases are infrequent — major design has been stable for a decade. - Supports Python 3.7+ on recent releases; 3.x line dropped Python 2 in
2.8.2(2021). - Updates mostly cover IANA tzdata refreshes (
tzdatapackage now does this better) and stdlib API changes. - The library does NOT follow strict semver —
2.xminor bumps occasionally include behavior changes around ambiguous parsing.
Package metadata
- Maintainer: Paul Ganssle, Yaron de Leeuw, et al.
- Project home: github.com/dateutil/dateutil
- Docs: dateutil.readthedocs.io
- PyPI: pypi.org/project/python-dateutil
- License: Apache-2.0 / BSD-3-Clause dual
- Governance: community-maintained
- First released: 2003
- Downloads: consistently top 10 on PyPI (transitive via
pandas,boto3,matplotlib)
Optional dependencies & extras
six— required at runtime for some legacy Python 2/3 compat shims. Pure Python.- No optional extras.
Alternatives
| Package | Trade-off |
|---|---|
Stdlib datetime + zoneinfo | Python 3.9+ has IANA tz built-in. Use for new code where format is known. |
arrow | Higher-level wrapper. Nicer API but heavier. Many recent libraries use arrow as their datetime layer. |
pendulum | Most ambitious — drop-in datetime replacement with tz everywhere. Niche but loyal users. |
whenever | Modern (2024+) typed datetime library; strict semantics. Compelling for new code. |
dateparser | More aggressive natural-language parsing ("yesterday", "in 3 days"). Slower than dateutil. |
Common gotchas
parser.parse()is permissive — sometimes too permissive.parse("9/4/2020")returns September 4 on US locale machines, April 9 elsewhere. Passdayfirst=Trueoryearfirst=Trueto disambiguate.parse("12345")returns a date —dateutilinterprets it as an integer year/timestamp depending on context. Validate input length first.relativedeltaadds calendar units;timedeltaadds wall-clock seconds.today + relativedelta(months=1)gives Feb 28 from Jan 28 (or Feb 29 in leap years);today + timedelta(days=30)gives Jan 30 → Feb 27. Pick the right one.- Timezone parsing requires a
tzinfosmapping when the string contains a tz abbreviation (EST,PDT, …). Abbreviations are ambiguous — usedateutil.tz.gettz("America/New_York")directly. rruleis verbose but precise. Build it once and reuse; constructing per query is wasteful.tz.tzlocal()reads the OS timezone — in Docker without/etc/localtimeyou get UTC. In tests, freeze withtz.gettz("UTC").- Two-digit years.
parse("01/02/02")defaults to 2002. Setparserinfo(yearfirst=True)for ambiguous YY-MM-DD strings.
Real-world recipes
The five recipes target the daily-use surface — parsing, calendar arithmetic, timezones, and recurrence.
Recipe 1 — Parse arbitrary date strings.
from dateutil import parser
for s in [
"2026-05-31T14:32:00Z",
"May 31, 2026 2:32 PM",
"31/5/2026 14:32",
"2026-W22-7",
"Tue, 31 May 2026 14:32:00 +0000",
]:
print(repr(s), "->", parser.parse(s, dayfirst=True))
Output:
'2026-05-31T14:32:00Z' -> 2026-05-31 14:32:00+00:00
'May 31, 2026 2:32 PM' -> 2026-05-31 14:32:00
'31/5/2026 14:32' -> 2026-05-31 14:32:00
'2026-W22-7' -> 2026-05-31 00:00:00
'Tue, 31 May 2026 14:32:00 +0000' -> 2026-05-31 14:32:00+00:00
dayfirst=True resolves DD/MM/YYYY ambiguity. The ISO week format YYYY-Www-D is also accepted.
Recipe 2 — Calendar-aware arithmetic with relativedelta.
from datetime import date
from dateutil.relativedelta import relativedelta, MO
today = date(2026, 5, 31)
print(today + relativedelta(months=1)) # natural month
print(today + relativedelta(years=1, months=-2, days=+10)) # combined
print(today + relativedelta(day=31)) # last day of THIS month
print(today + relativedelta(months=1, day=31)) # last day of next month
print(today + relativedelta(weekday=MO(+1))) # next Monday
print(today + relativedelta(weekday=MO(-1))) # previous Monday
Output:
2026-06-30
2027-04-10
2026-05-31
2026-06-30
2026-06-01
2026-05-25
day=N clamps to the maximum valid day; weekday=MO(+1) selects the next Monday. +1 means "the next one if today isn't already that weekday".
Recipe 3 — Timezone-aware parsing.
from dateutil import parser, tz
tzinfos = {"PST": tz.gettz("America/Los_Angeles"), "EST": tz.gettz("America/New_York")}
dt = parser.parse("2026-05-31 09:30 PST", tzinfos=tzinfos)
print(dt) # 09:30 in LA
print(dt.astimezone(tz.UTC)) # converted to UTC
print(dt.astimezone(tz.gettz("Asia/Tokyo")))
Output:
2026-05-31 09:30:00-07:00
2026-05-31 16:30:00+00:00
2026-06-01 01:30:00+09:00
Always pass tzinfos when abbreviations are present — otherwise the parser raises or returns naive datetimes.
Recipe 4 — Recurring schedule with rrule.
from datetime import date
from dateutil.rrule import rrule, MONTHLY, MO
# First Monday of each month for 6 occurrences
schedule = list(rrule(
freq=MONTHLY,
bymonthday=range(1, 8),
byweekday=MO,
dtstart=date(2026, 5, 1),
count=6,
))
for d in schedule:
print(d.date())
Output:
2026-05-04
2026-06-01
2026-07-06
2026-08-03
2026-09-07
2026-10-05
rrule is RFC 5545 — every iCalendar-supported recurrence pattern is expressible.
Recipe 5 — ISO 8601 round-trip.
from datetime import datetime, timezone
from dateutil import parser
original = datetime(2026, 5, 31, 14, 32, 0, tzinfo=timezone.utc)
s = original.isoformat()
parsed = parser.isoparse(s) # stricter than .parse(); fails on non-ISO
print(s)
print(parsed)
print(parsed == original)
Output:
2026-05-31T14:32:00+00:00
2026-05-31 14:32:00+00:00
True
parser.isoparse() is strict ISO 8601 — prefer it over the permissive parse() when you control the input format.
Performance tuning
parser.parse()is slow by HTTP-handler standards (~50-200 µs per call). For high-volume input, preferisoparse()(~5x faster) or stdlibdatetime.strptime(fmt, s)when format is known.- Cache
rruleobjects — construction is moderately expensive; iteration is cheap. - Use
tz.gettz("UTC")once per process — repeated lookups parse the OS tz file each time. - Don't import
dateutilin cold-start-sensitive code (Lambda) — the import itself is ~30 ms. Lazy-import inside the function body.
Version migration guide
2.7 → 2.8— Python 2 EOL bookkeeping; somerelativedeltaedge cases tightened.2.8.0 → 2.8.2— Python 2 support dropped; minimum Python 3.6 then 3.7.2.8.2 → 2.9.0— minimum Python 3.7.parser.parsetightened around ambiguous 2-digit years.
# Pre-2.9 — sometimes returned naive datetime for inputs with explicit Z
parser.parse("2026-05-31T00:00:00Z") # tz=tzutc()
# 2.9+ — same behavior, but isoparse() is now the documented strict path
parser.isoparse("2026-05-31T00:00:00Z") # tz=tzutc()
Output: stable across the 2.x line; prefer isoparse for ISO-only input.
Security considerations
parser.parse()can be slow on adversarial input. Pathological strings ("123" × N) can spend disproportionate CPU. Cap input length for untrusted sources.- Timezone abbreviations are ambiguous —
ESTis both US Eastern Standard and Australian Eastern Standard. Never trusttzinfosdefaults from untrusted input. parse(default=)lets you seed missing fields. If the string is "Monday", the date defaults to the current Monday — surprising and exploitable in scheduling.fuzzy=Trueparses date-like substrings out of free-form text. Powerful and dangerous: a log line containing "1 of 12345 errors" might parse to year 12345.
Testing & CI
from datetime import date
from dateutil.relativedelta import relativedelta
def test_month_end_arithmetic():
assert date(2026, 1, 31) + relativedelta(months=1) == date(2026, 2, 28)
assert date(2024, 1, 31) + relativedelta(months=1) == date(2024, 2, 29) # leap year
def test_iso_roundtrip():
from dateutil import parser
from datetime import datetime, timezone
dt = datetime.now(timezone.utc).replace(microsecond=0)
assert parser.isoparse(dt.isoformat()) == dt
Output: asserts hold across versions; protects against accidental regressions on the calendar-arithmetic behavior.
For tz tests, freeze the system clock with freezegun:
# pip install freezegun
from freezegun import freeze_time
@freeze_time("2026-05-31T12:00:00+00:00")
def test_today():
from datetime import datetime, timezone
assert datetime.now(timezone.utc).date().isoformat() == "2026-05-31"
Output: test passes regardless of CI machine clock.
Ecosystem integrations
pandas— usesdateutil.parserforpd.to_datetime.boto3— uses it for AWS response timestamps.matplotlib— uses it for axis date parsing.requests— usesemail.utils.parsedate_to_datetimeinstead, but many wrappers use dateutil.zoneinfo(stdlib 3.9+) — alternative tz source; pair with dateutil for tz-aware parsing.tzdata— runtime IANA tz data, especially useful on Windows where the OS tz database is incomplete.
Compatibility matrix
| Python | python-dateutil | Notes |
|---|---|---|
| 2.7 | 2.8.2 (frozen) | Final supported version. |
| 3.6 | 2.8.2 (frozen) | Final supported version. |
| 3.7 | 2.9.x | Lowest current floor. |
| 3.8 | 2.9.x | Stable. |
| 3.9 | 2.9.x | Stable. |
| 3.10 | 2.9.x | Stable. |
| 3.11 | 2.9.x | Stable. |
| 3.12 | 2.9.x | Stable. |
| 3.13 | 2.9.x | Stable; wheel universal. |
Production deployment
- Pin a minimum version in libraries (
python-dateutil>=2.8.2) but allow flexibility — the library updates infrequently. - Install
tzdataalongsidepython-dateutilon Alpine / Windows containers where the OS tz database is missing or incomplete. - Set
TZ=UTCin container environments — predictable behavior trumps localization. - Audit
parse()calls in input handlers — make suredayfirst/yearfirstis set deterministically. - Reach for
zoneinfo(stdlib) when targeting 3.9+ — it's faster thandateutil.tzfor repeated lookups.
When NOT to use this
- You know the format —
datetime.strptime("2026-05-31", "%Y-%m-%d")is ~10× faster thanparser.parse(). - You're on 3.9+ and need IANA timezones only — stdlib
zoneinfocovers it without an extra dep. - You need natural-language parsing ("tomorrow at 5") — use
dateparserinstead. - You want strict semver / typed datetimes — try
whenever(newer, smaller, strictly typed).
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
ValueError: Unknown string format | Input not date-shaped at all | Validate first; reject obvious garbage. |
parse("9/4/2020") returns wrong month | DD/MM vs MM/DD ambiguity | Pass dayfirst=True or yearfirst=True. |
| Tz abbreviation parsing returns naive datetime | Abbreviation not in tzinfos | Build a tzinfos dict mapping abbreviations to IANA zones. |
rrule schedule has zero entries | Mismatched bymonthday/byweekday | Inspect with list(rrule(...))[:10]; verify constraints intersect. |
| Tests behave differently in CI vs local | Container has no tz data | Install tzdata (PyPI) and set TZ=UTC. |
| Python 3.13 import warning | Deprecated alias used | Switch to documented imports under dateutil.parser / dateutil.rrule. |
See also
- Python: datetime — stdlib datetime fundamentals
- Official python-dateutil docs
- RFC 5545 — iCalendar recurrence rules