A modern Python RFC 5545 iCalendar library with built-in recurring event support — no companion libraries needed.
from ical.calendar_stream import IcsCalendarStream
cal = IcsCalendarStream.calendar_from_ics(ics_content)
# Recurring events are automatically expanded — just iterate
for event in cal.timeline:
print(event.start, event.summary)Most Python iCalendar libraries require two separate packages to handle recurring events:
# The typical ecosystem approach — two libraries, two APIs
import icalendar
import recurring_ical_events
cal = icalendar.Calendar.from_ical(ics_content)
events = recurring_ical_events.of(cal).between(start, end)ical handles this natively with a single, unified Timeline interface, with a Pythonic API and validated inputs.
| Feature | ical |
icalendar |
ics.py |
|---|---|---|---|
| Built-in recurrence expansion | ✅ | ❌ needs recurring-ical-events |
❌ |
| Incremental recurrence & timezone caching | ✅ | ❌ | ❌ |
Pythonic attribute access (event.start, event.summary) |
✅ | ❌ (event.get('DTSTART').dt) |
✅ |
| Input validation with clear error messages | ✅ | ❌ | ❌ |
Full type annotations (py.typed) |
✅ strict ty |
✅ v7+ | ❌ |
| Application-level store API | ✅ | ❌ | ❌ |
| RFC 7986 / RFC 6868 / RFC 8536 | ✅ | partial | ❌ |
| Active maintenance | ✅ | ✅ |
- Home Assistant — powers the Local Calendar, Remote Calendar, and Google Calendar integrations (including serving locally-synced calendars for performance)
uv add icalOr with pip:
pip install icalRequires Python 3.11+.
Fetching calendars from a url asynchronously (see below) requires the optional async extra, which pulls in aiohttp:
pip install ical[async]Parse a calendar file and iterate over events in chronological order, with recurring events automatically expanded:
from pathlib import Path
from ical.calendar_stream import IcsCalendarStream
from ical.exceptions import CalendarParseError
filename = Path("calendar.ics")
with filename.open() as ics_file:
try:
cal = IcsCalendarStream.calendar_from_ics(ics_file.read())
except CalendarParseError as err:
print(f"Failed to parse '{filename}': {err}")
else:
for event in cal.timeline:
print(event.start, event.summary)Fetching a remote calendar over HTTP without blocking the event loop requires the optional ical[async] extra:
from ical.calendar_stream import IcsCalendarStream
cal = await IcsCalendarStream.calendar_from_url("https://example.com/calendar.ics")
for event in cal.timeline:
print(event.start, event.summary)An existing aiohttp.ClientSession can be passed in via the session argument to reuse connection pooling; otherwise a session is created and closed automatically for the request.
from datetime import date
from ical.calendar import Calendar
from ical.event import Event
cal = Calendar()
cal.events.append(
Event(summary="Team standup", start=date(2024, 1, 15), end=date(2024, 1, 16)),
)
for event in cal.timeline:
print(event.summary)from pathlib import Path
from ical.calendar_stream import IcsCalendarStream
with Path("output.ics").open("w") as f:
f.write(IcsCalendarStream.calendar_to_ics(cal))Recurring events are stored once in the calendar but automatically expanded by the Timeline:
from datetime import date
from ical.calendar import Calendar
from ical.event import Event
from ical.types.recur import Recur
cal = Calendar()
cal.events.append(
Event(
summary="Weekly standup",
start=date(2024, 1, 15),
end=date(2024, 1, 16),
rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=10"),
)
)
# All 10 occurrences are expanded automatically
for event in cal.timeline:
print(event.start, event.summary)For managing calendar state in an application (ensuring timezones are set correctly, editing individual instances of recurring events, etc.), use ical.store:
from ical.store import EventStore
from ical.event import Event
from datetime import datetime, timezone
store = EventStore()
store.add(Event(summary="Meeting", start=datetime(2024, 1, 15, 9, tzinfo=timezone.utc)))See the full documentation for the complete API reference.
ical is optimized for applications like Home Assistant that repeatedly check a calendar for today's events or the next upcoming event — even when the calendar has thousands of events or recurring rules (like birthdays and weekly meetings) that started years ago:
- Only builds events you actually read: Scanning a timeline compares lightweight start/end times and only constructs full
Eventobjects for occurrences that match your query. - Caches recurring rules and timezone transitions: Expanding a decades-old recurrence rule takes work the first time, so
icalcaches expanded dates and timezone daylight-saving transitions as you query. Subsequent lookups reuse that cache instead of recalculating from the beginning. - Skips past history on range queries: Methods like
overlapping(),on_date(), andactive_after()jump directly to your start date rather than walking through years of past occurrences one by one.
| Workload | ical |
icalendar + recurring-ical-events |
|---|---|---|
| Recurring range query (100 recurring series, cold / first query) | ~252 ms | ~254 ms |
| Recurring range query (100 recurring series, warm) | ~2.4 ms (~41,000 series/s) | ~254 ms (~390 series/s) |
| Large recurring calendar query (1,000 recurring series, cold / first query) | ~2,370 ms | ~2,320 ms |
| Large recurring calendar query (1,000 recurring series, warm) | ~28 ms (~35,000 series/s) | ~2,320 ms (~430 series/s) |
Non-recurring range query (2,000 events with custom VTIMEZONE) |
~8.9 ms (~225,000 events/s) | ~42 ms (~47,000 events/s) |
.ics serialization (500 VEVENTs) |
~6.9 ms (~72,000 events/s) | ~10.6 ms (~47,000 events/s) |
.ics parsing + Pydantic validation (500 VEVENTs) |
~29 ms (~17,000 events/s) | ~18.5 ms (unvalidated AST) |
We publish detailed technical guides exploring the library's design:
- RFC 5545 — Internet Calendaring and Scheduling Core Object Specification (iCalendar)
- RFC 6868 — Parameter Value Encoding in iCalendar and vCard
- RFC 7986 — New Properties for iCalendar
- RFC 8536 — The Time Zone Information Format (TZif)
ical is designed for applications that need a complete, modern solution. You may prefer an alternative if:
icalendar— You need low-level control over raw iCalendar components, jCal (JSON) support, or maximum ecosystem compatibility. You prefer to expand recurring events manually and don't mind a second library (recurring-ical-events). You need to support legacy Python versions (3.8+) thaticaldoes not target.ics.py— You need a simple read-only script and do not require recurrence support. Note: no stable release since 0.7.2 (August 2021).
See CONTRIBUTING.md for setup and development instructions.