diff --git a/examples/python-dateutil/README.md b/examples/python-dateutil/README.md
new file mode 100644
index 0000000..487a8df
--- /dev/null
+++ b/examples/python-dateutil/README.md
@@ -0,0 +1,18 @@
+# python-dateutil Examples
+
+Each sub-directory contains a self-contained example. The order in
+which the examples are to appear is specified in `order.json` (an
+array of directory names in the expected order).
+
+In each example directory you'll find:
+
+* `config.toml` - must conform to the specification outlined here:
+ https://docs.pyscript.net/latest/user-guide/configuration/ This is
+ parsed and ultimately turned into a JSON representation as part of
+ the package's API object.
+* `setup.py` - Python code for contextual and environmental setup,
+ NOT SEEN BY THE END USER, but is run before the `code.py` code is
+ evaluated. Allows us to create useful (IPython) shims, avoid
+ repeating boilerplate and whatnot.
+* `code.py` - the actual code added to the editor which forms the
+ practical example of using the package.
diff --git a/examples/python-dateutil/order.json b/examples/python-dateutil/order.json
new file mode 100644
index 0000000..29d2810
--- /dev/null
+++ b/examples/python-dateutil/order.json
@@ -0,0 +1,4 @@
+[
+ "parsing_and_relativedelta",
+ "recurrence_rules"
+]
diff --git a/examples/python-dateutil/parsing_and_relativedelta/code.py b/examples/python-dateutil/parsing_and_relativedelta/code.py
new file mode 100644
index 0000000..58fd905
--- /dev/null
+++ b/examples/python-dateutil/parsing_and_relativedelta/code.py
@@ -0,0 +1,108 @@
+"""
+A friendly tour of python-dateutil.
+
+The standard library's `datetime` is great for representing instants,
+but it's awkward when you need to *parse* messy human input or do
+calendar-aware arithmetic like "the third Friday of next month."
+That's where `dateutil` shines.
+
+Docs: https://dateutil.readthedocs.io/en/stable/
+"""
+from IPython.core.display import display, HTML
+
+# Package imports for the example.
+from datetime import datetime, date
+from dateutil.parser import parse, parserinfo
+from dateutil.relativedelta import relativedelta, MO, FR, SU
+
+
+# ---------------------------------------------------------------------
+# Section 1: Parsing dates from almost any string format.
+# ---------------------------------------------------------------------
+
+heading("1. Parsing messy date strings")
+note(
+ "A small batch of timestamps written by different people, in "
+ "different formats. dateutil.parser.parse handles them all."
+)
+
+raw_timestamps = [
+ "2024-03-15T09:41:00",
+ "March 15, 2024 9:41 AM",
+ "15/03/2024 09:41",
+ "Fri, 15 Mar 2024 09:41:00 +0100",
+ "20240315T094100",
+]
+
+rows = []
+for raw in raw_timestamps:
+ parsed = parse(raw)
+ rows.append(f"
{raw} | {parsed} |
")
+
+display(HTML(
+ ""
+ "| Input | Parsed datetime |
"
+ + "".join(rows) +
+ "
"
+), append=True)
+
+# Fuzzy parsing pulls a date out of surrounding prose.
+sentence = "The package was shipped on Tuesday, April 12th 2022 at 5pm."
+note(f"Fuzzy parse of: {sentence}")
+note(f"Result: {parse(sentence, fuzzy=True)}")
+
+# Day-first vs. month-first ambiguity.
+note(
+ "The string '04-05-2024' is ambiguous. dateutil lets you steer it: "
+ f"default reads it as {parse('04-05-2024').date()}, "
+ f"while dayfirst=True reads it as "
+ f"{parse('04-05-2024', dayfirst=True).date()}."
+)
+
+# ---------------------------------------------------------------------
+# Section 2: relativedelta for calendar-aware arithmetic.
+# ---------------------------------------------------------------------
+
+heading("2. relativedelta: calendar-aware date math")
+note(
+ "Unlike timedelta (which only knows about fixed durations), "
+ "relativedelta understands months, years, and weekday targets."
+)
+
+today = date(2024, 3, 15)
+note(f"Anchor date: {today} (a Friday).")
+
+examples = [
+ ("Three months from now",
+ today + relativedelta(months=+3)),
+ ("One year and two months ago",
+ today + relativedelta(years=-1, months=-2)),
+ ("Next Monday",
+ today + relativedelta(weekday=MO(+1), days=+1)),
+ ("Last Friday of this month",
+ today + relativedelta(day=31, weekday=FR(-1))),
+ ("First Sunday of next month",
+ today + relativedelta(months=+1, day=1, weekday=SU(+1))),
+ ("End-of-month rollover from Jan 31",
+ date(2024, 1, 31) + relativedelta(months=+1)),
+]
+
+rows = [
+ f"| {label} | {value} |
"
+ for label, value in examples
+]
+display(HTML(
+ ""
+ "| Question | Answer |
"
+ + "".join(rows) +
+ "
"
+), append=True)
+
+# The difference between two dates as a calendar-aware delta.
+born = date(1995, 7, 22)
+age = relativedelta(today, born)
+note(
+ f"Someone born on {born} is "
+ f"{age.years} years, {age.months} months, "
+ f"and {age.days} days old on {today}."
+)
diff --git a/examples/python-dateutil/parsing_and_relativedelta/config.toml b/examples/python-dateutil/parsing_and_relativedelta/config.toml
new file mode 100644
index 0000000..9a094d1
--- /dev/null
+++ b/examples/python-dateutil/parsing_and_relativedelta/config.toml
@@ -0,0 +1 @@
+packages = ["python-dateutil"]
diff --git a/examples/python-dateutil/parsing_and_relativedelta/setup.py b/examples/python-dateutil/parsing_and_relativedelta/setup.py
new file mode 100644
index 0000000..eb25948
--- /dev/null
+++ b/examples/python-dateutil/parsing_and_relativedelta/setup.py
@@ -0,0 +1,37 @@
+"""Shim IPython's display API onto PyScript and import dateutil pieces."""
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+ipython = types.ModuleType("IPython")
+core = types.ModuleType("IPython.core")
+core_display = types.ModuleType("IPython.core.display")
+core_display.display = display
+core_display.HTML = HTML
+ipython.core = core
+core.display = core_display
+ipython.version_info = (9, 0, 2, '')
+ipython.get_ipython = lambda: None
+ipython.display = core_display
+sys.modules["IPython"] = ipython
+sys.modules["IPython.core"] = core
+sys.modules["IPython.core.display"] = core_display
+sys.modules["IPython.display"] = core_display
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}"), append=True)
+
+
+def note(text):
+ display(HTML(f"{text}
"), append=True)
diff --git a/examples/python-dateutil/recurrence_rules/code.py b/examples/python-dateutil/recurrence_rules/code.py
new file mode 100644
index 0000000..4579cd5
--- /dev/null
+++ b/examples/python-dateutil/recurrence_rules/code.py
@@ -0,0 +1,132 @@
+# ---------------------------------------------------------------------
+# Section 3: Recurrence rules - schedules, holidays, and exceptions.
+# ---------------------------------------------------------------------
+# Package imports for the example.
+from datetime import datetime
+from dateutil.parser import parse
+from dateutil.rrule import (
+ rrule, rruleset, rrulestr,
+ YEARLY, MONTHLY, WEEKLY, DAILY,
+ MO, TU, WE, TH, FR, SA, SU,
+)
+
+
+heading("3. rrule: generate schedules from recurrence rules")
+note(
+ "rrule implements the iCalendar RECUR specification. You can "
+ "describe a schedule declaratively and iterate over its "
+ "occurrences, just like a calendar app does behind the scenes."
+)
+
+# A standing meeting: every other Tuesday and Thursday at 10:00,
+# for the next eight occurrences.
+standup = rrule(
+ WEEKLY,
+ interval=2,
+ byweekday=(TU, TH),
+ count=8,
+ dtstart=datetime(2024, 4, 2, 10, 0),
+)
+
+note("Bi-weekly stand-up on Tuesdays and Thursdays:")
+display(HTML(
+ ""
+ + "".join(f"- {when:%a %Y-%m-%d %H:%M}
" for when in standup)
+ + "
"
+), append=True)
+
+# Every Friday the 13th in the 21st century (first six).
+unlucky = rrule(
+ YEARLY,
+ byweekday=FR,
+ bymonthday=13,
+ count=6,
+ dtstart=datetime(2024, 1, 1),
+)
+
+note("The next six Friday the 13ths:")
+display(HTML(
+ ""
+ + "".join(f"- {when:%A, %B %d, %Y}
" for when in unlucky)
+ + "
"
+), append=True)
+
+# US Presidential Election Day: first Tuesday after a Monday in
+# November, every 4 years. The bymonthday=(2..8) clause forces the
+# Tuesday to fall after the first Monday of the month.
+elections = rrule(
+ YEARLY,
+ interval=4,
+ bymonth=11,
+ byweekday=TU,
+ bymonthday=(2, 3, 4, 5, 6, 7, 8),
+ count=4,
+ dtstart=datetime(2024, 1, 1),
+)
+
+note("Upcoming US Presidential Election Days:")
+display(HTML(
+ ""
+ + "".join(f"- {when:%A, %B %d, %Y}
" for when in elections)
+ + "
"
+), append=True)
+
+# ---------------------------------------------------------------------
+# rruleset: combine rules and add or remove specific dates.
+# ---------------------------------------------------------------------
+
+heading("4. rruleset: combining rules with exceptions")
+note(
+ "Real-world calendars are rarely a single clean rule. rruleset "
+ "lets you union multiple rules, add one-off dates with rdate, "
+ "and exclude dates with exdate."
+)
+
+# Daily for two weeks, but skip weekends and one specific holiday.
+schedule = rruleset()
+schedule.rrule(rrule(
+ DAILY,
+ count=14,
+ dtstart=datetime(2024, 7, 1, 9, 0),
+))
+# Exclude all Saturdays and Sundays in this range.
+schedule.exrule(rrule(
+ DAILY,
+ byweekday=(SA, SU),
+ dtstart=datetime(2024, 7, 1, 9, 0),
+ until=datetime(2024, 7, 15, 9, 0),
+))
+# Exclude US Independence Day, which falls inside the window.
+schedule.exdate(datetime(2024, 7, 4, 9, 0))
+
+note("Working days in the first half of July 2024:")
+display(HTML(
+ ""
+ + "".join(f"- {when:%a %Y-%m-%d}
" for when in schedule)
+ + "
"
+), append=True)
+
+# ---------------------------------------------------------------------
+# rrulestr: parse an iCalendar RRULE string directly.
+# ---------------------------------------------------------------------
+
+heading("5. rrulestr: parse iCalendar RRULE strings")
+note(
+ "If you already have an RFC 5545 RRULE string (for example, from "
+ "an .ics file), rrulestr parses it directly."
+)
+
+ical_rule = """
+DTSTART:20240115T080000
+RRULE:FREQ=MONTHLY;BYDAY=3MO;COUNT=6
+"""
+note("Parsing: FREQ=MONTHLY;BYDAY=3MO;COUNT=6 "
+ "(third Monday of the month, six times):")
+
+occurrences = list(rrulestr(ical_rule))
+display(HTML(
+ ""
+ + "".join(f"- {when:%A, %B %d, %Y at %H:%M}
"
+ for when in occurrences)
+ + "
"
+), append=True)
diff --git a/examples/python-dateutil/recurrence_rules/config.toml b/examples/python-dateutil/recurrence_rules/config.toml
new file mode 100644
index 0000000..9a094d1
--- /dev/null
+++ b/examples/python-dateutil/recurrence_rules/config.toml
@@ -0,0 +1 @@
+packages = ["python-dateutil"]
diff --git a/examples/python-dateutil/recurrence_rules/setup.py b/examples/python-dateutil/recurrence_rules/setup.py
new file mode 100644
index 0000000..0029200
--- /dev/null
+++ b/examples/python-dateutil/recurrence_rules/setup.py
@@ -0,0 +1,20 @@
+"""Lightweight setup for the second example. No IPython shim here."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}"), append=True)
+
+
+def note(text):
+ display(HTML(f"{text}
"), append=True)
+