python-dateutil
python-dateutil extends the standard datetime module with the pieces it never grew: a parser that reads dates out of almost any string, relativedelta for calendar-aware arithmetic (next month, last Friday of the month), rrule for iCalendar-style recurrence rules, timezone implementations that read tzfile and TZ strings, and an Easter calculator. It has been around since 2003, is imported (as dateutil, not python-dateutil) by a huge share of the Python ecosystem, and pandas depends on it, which is a big part of its download numbers.
Keep it for the three things nothing else does as well: fuzzy string parsing, relativedelta arithmetic, and rrule recurrences. For plain timezone handling or ISO parsing on modern Python, the standard library has caught up and you do not need the dependency.
Use it if
- You receive dates as messy strings ('Sat Oct 11 17:13:46 UTC 2003', '4th of July 2026') and need one call that copes with most formats
- You need calendar arithmetic the timedelta class cannot do: add one month, jump to the last weekday of a month, or compute a human-style year/month/day difference between two dates
- You need recurrence rules ('every second Tuesday until December') and want the RFC 5545 superset that rrule implements, including parsing RRULE strings from calendar feeds
- You maintain code that must run on old Python: the package still supports back to 2.7, which almost nothing else does
- All you parse is ISO 8601: datetime.fromisoformat covers most ISO strings natively on Python 3.11+, and dateutil.parser is slow and permissive by comparison
- You only need timezone objects: stdlib zoneinfo (Python 3.9+) handles named zones without any third-party dependency, and the dateutil docs themselves point modern code there
- Maintenance pace bothers you: the last release was 2.9.0.post0 in March 2024, the one before that in 2021, there are 469 open issues and PRs, and the package still drags in six for Python 2 compatibility
- You are parsing user input where guessing is dangerous: parse() happily reads ambiguous strings like '01/02/03' with silent US-style assumptions, so validated input should use explicit strptime formats instead
Setup reality
pip install python-dateutil, then import dateutil; the mismatch between install name and import name trips up every requirements scanner and some new users. It is pure Python with no compiled parts, but it installs six (a Python 2/3 shim) as a dependency in 2026, which security scanners occasionally flag as a smell. Timezone data comes from the OS tzfile database plus a bundled fallback, so results on minimal Docker images can differ from your laptop unless tzdata is installed.
Patterns
Parse a date out of almost any stringparse-any-date-string
from dateutil import parser
parser.parse("Sat Oct 11 17:13:46 UTC 2003")
# datetime.datetime(2003, 10, 11, 17, 13, 46, tzinfo=tzutc())
parser.parse("4th of July 2026")
# datetime.datetime(2026, 7, 4, 0, 0)Fields missing from the string are filled from today's date at 00:00, so parse('12:30') returns today at 12:30; pass default= to control that.
Handle DD/MM/YYYY dates explicitlyparse-day-first
from dateutil import parser
parser.parse("10/09/2003") # Oct 9 (US order)
parser.parse("10/09/2003", dayfirst=True) # Sep 10The default is month-first, silently. Any code parsing European-style dates without dayfirst=True has a latent bug that only shows on days 1-12.
Parse ISO 8601 strictlystrict-iso-parse
from dateutil import parser
parser.isoparse("2026-08-05T14:30:00+05:30")
# datetime with tzoffset(None, 19800)isoparse rejects non-ISO input instead of guessing and is faster than parse(). On Python 3.11+ datetime.fromisoformat does the same job without the dependency.
Parse named timezone abbreviationsparse-timezone-abbreviations
from dateutil import parser, tz
tzinfos = {"IST": tz.gettz("Asia/Kolkata"), "CST": tz.gettz("America/Chicago")}
parser.parse("2026-08-05 14:30 IST", tzinfos=tzinfos)Only UTC/GMT and numeric offsets are understood out of the box; unknown abbreviations like IST produce a naive datetime plus a warning unless you supply tzinfos.
Add months or years correctlyadd-months
from datetime import date
from dateutil.relativedelta import relativedelta
date(2026, 1, 31) + relativedelta(months=+1) # date(2026, 2, 28)
date(2026, 8, 5) + relativedelta(years=+1, months=+2)relativedelta clamps to the last valid day instead of overflowing, which timedelta cannot express at all. Plural arguments (months=) shift; singular (month=) set an absolute value.
Jump to the next or last weekdaynext-weekday
from datetime import date
from dateutil.relativedelta import relativedelta, FR
date(2026, 8, 5) + relativedelta(weekday=FR) # next Friday
date(2026, 8, 5) + relativedelta(day=31, weekday=FR(-1)) # last Friday of monthweekday=FR includes the start date if it already is a Friday; use FR(+1) semantics carefully when 'strictly next' matters.
Human-style difference between two datescalendar-diff
from datetime import date
from dateutil.relativedelta import relativedelta
delta = relativedelta(date(2026, 8, 5), date(1994, 3, 11))
print(delta.years, delta.months, delta.days) # 32 4 25This is the correct way to compute an age in years/months/days; subtracting dates gives only a day count.
Generate recurring dates with rrulerecurrence-rule
from datetime import datetime
from dateutil.rrule import rrule, WEEKLY, MO, WE
list(rrule(WEEKLY, byweekday=(MO, WE), count=4,
dtstart=datetime(2026, 8, 3)))
# Mon 3rd, Wed 5th, Mon 10th, Wed 12thWithout dtstart the rule starts from now including the current time, which makes results non-deterministic in tests; always pass dtstart.
Parse an RRULE string from a calendar feedparse-ical-rrule
from datetime import datetime
from dateutil.rrule import rrulestr
rule = rrulestr("FREQ=MONTHLY;BYDAY=2TU;COUNT=6",
dtstart=datetime(2026, 8, 1))
rule.between(datetime(2026, 8, 1), datetime(2027, 1, 1))between() excludes both endpoints by default; pass inc=True to include them. This bites everyone once.
Build timezone-aware datetimestimezone-aware-now
from datetime import datetime
from dateutil import tz
nyc = tz.gettz("America/New_York")
datetime(2026, 11, 1, 1, 30, tzinfo=nyc, fold=1) # DST-ambiguous time
datetime.now(tz.UTC)gettz reads the OS zone database with a bundled fallback. On Python 3.9+ stdlib zoneinfo.ZoneInfo does this without dateutil; use it in new code.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| arrow | PyPI | You want a friendlier all-in-one datetime object with humanize() and span helpers rather than extensions to stdlib datetime. |
| pendulum | PyPI | You want a datetime drop-in subclass with sane timezone defaults and duration formatting; check its release activity first. |
| whenever | PyPI | You want a strict, typed API that makes naive vs aware mistakes impossible and is fast (Rust-backed). |