Async API#

The caldav library provides an async API for use with Python’s asyncio. This is useful when you need to:

  • Make concurrent requests to the server

  • Integrate with async web frameworks (FastAPI, aiohttp, etc.)

  • Build responsive applications that don’t block on I/O

Caveats#

Async IO was introduced in version 3.0, 2026-03-03, without being tested in any production environments, and it was done by a developer not having much experience with async usage, and probably with a bit too much trust in AI-assistance. The quality of the async code has been lifted significantly in 3.2, but rough edges are still to be expected. Test it very well in a staging environment before using it in production environments. It’s probably a good idea to wait until version 4.0 before using it in very sharp production settings.

We’ve ended up with some hybrid design pattern inspired by “Sans-IO”. There is a dual DAVClient vs AsyncDAVClient with a common baseclass. On the other classes, all methods that involves or may involve IO will deliver an awaitable coroutine in async mode. I’m not sure that the current dsign is the best, and the design may be revisited and shaken up in 4.0. (Claude suggests that a async-first-generate-sync is the best option for CalDAV).

There may still be sharp edges - as of v3.2, there is 40kb of async integration test code compared to 164kb of sync integration test code, so most likely some of the less travelled code paths will blow up when using it in async mode. File an issue, and I’ll prioritize fixing it! I will work more on this in an upcoming version 3.2.1.

Quick Start#

The async API is available through the caldav.aio module:

import asyncio
from caldav import aio

async def main():
    async with await aio.get_async_davclient() as client:
        principal = await client.get_principal()
        calendars = await principal.get_calendars()
        for cal in calendars:
            print(f"Calendar: {await cal.get_display_name()}")
            events = await cal.get_events()
            print(f"  {len(events)} events")

asyncio.run(main())

The async API mirrors the sync API, but all I/O operations are async methods that must be awaited.

Available Classes#

The caldav.aio module exports:

Client:

  • AsyncDAVClient - The main client class

  • AsyncDAVResponse - Response wrapper

  • get_async_davclient() - Factory function (recommended)

Calendar Objects:

  • AsyncEvent - Calendar event

  • AsyncTodo - Task/todo item

  • AsyncJournal - Journal entry

  • AsyncFreeBusy - Free/busy information

Collections:

  • AsyncCalendar - A calendar

  • AsyncCalendarSet - Collection of calendars

  • AsyncPrincipal - User principal

Scheduling (:rfc:`6638`):

  • AsyncScheduleInbox - Incoming invitations

  • AsyncScheduleOutbox - Outgoing invitations

Example: Working with Calendars#

import asyncio
from caldav import aio
from datetime import datetime, date

async def calendar_demo():
    async with await aio.get_async_davclient() as client:
        principal = await client.get_principal()

        # Create a new calendar
        my_calendar = await principal.make_calendar(
            name="My Async Calendar"
        )

        # Add an event
        event = await my_calendar.add_event(
            dtstart=datetime(2025, 6, 15, 10, 0),
            dtend=datetime(2025, 6, 15, 11, 0),
            summary="Team meeting"
        )

        # Search for events
        events = await my_calendar.search(
            event=True,
            start=date(2025, 6, 1),
            end=date(2025, 7, 1)
        )
        print(f"Found {len(events)} events")

        # Clean up
        await my_calendar.delete()

asyncio.run(calendar_demo())

Example: Parallel Operations#

One of the main benefits of async is the ability to run operations concurrently:

import asyncio
from caldav import aio

async def fetch_all_events():
    async with await aio.get_async_davclient() as client:
        principal = await client.get_principal()
        calendars = await principal.get_calendars()

        # Fetch events from all calendars in parallel
        tasks = [cal.get_events() for cal in calendars]
        results = await asyncio.gather(*tasks)

        for cal, events in zip(calendars, results):
            print(f"{await cal.get_display_name()}: {len(events)} events")

asyncio.run(fetch_all_events())

Migration from Sync to Async#

The async API closely mirrors the sync API. Here are the key differences:

  1. Import from ``caldav.aio``:

    # Sync
    from caldav import DAVClient, get_davclient
    
    # Async
    from caldav import aio
    # Use: aio.AsyncDAVClient, aio.get_async_davclient()
    
  2. Use ``async with`` for context manager:

    # Sync
    with get_davclient() as client:
        ...
    
    # Async
    async with await aio.get_async_davclient() as client:
        ...
    
  3. Await all I/O operations:

    # Sync
    principal = client.get_principal()
    calendars = principal.get_calendars()
    events = calendar.get_events()
    
    # Async
    principal = await client.get_principal()
    calendars = await principal.get_calendars()
    events = await calendar.get_events()
    
  4. Property access for cached data remains sync:

    Properties that don’t require I/O (like url, name, data) are still regular properties:

    # These work the same in both sync and async
    print(calendar.url)
    print(calendar.name)
    print(event.data)
    

Method Reference#

The async classes have the same methods as their sync counterparts. All methods that perform I/O are async and must be awaited:

AsyncDAVClient:

  • await client.get_principal() - Get the principal

  • client.calendar(url=...) - Get a calendar by URL (no await, no I/O)

AsyncPrincipal:

  • await principal.get_calendars() - List all calendars

  • await principal.make_calendar(name=...) - Create a calendar

  • await principal.calendar(name=...) - Find a calendar

AsyncCalendar:

  • await calendar.get_events() - Get all events

  • await calendar.get_todos() - Get all todos

  • await calendar.search(...) - Search for objects

  • await calendar.add_event(...) - Create an event

  • await calendar.add_todo(...) - Create a todo

  • await calendar.get_event_by_uid(uid) - Find event by UID

  • await calendar.delete() - Delete the calendar

  • await calendar.get_supported_components() - Get supported types

AsyncEvent, AsyncTodo, AsyncJournal:

  • await obj.load() - Load data from server

  • await obj.save() - Save changes to server

  • await obj.delete() - Delete the object

  • await todo.complete() - Mark todo as complete

Backward Compatibility#

The sync API (caldav.DAVClient, caldav.get_davclient()) continues to work exactly as before. The sync API now uses the async implementation internally, with a thin sync wrapper.

This means:

  • Existing sync code works without changes

  • You can migrate to async gradually

  • Both sync and async code can coexist in the same project

Example Files#

See the examples/ directory for complete examples:

  • examples/async_usage_examples.py - Comprehensive async examples

  • examples/basic_usage_examples.py - Sync examples (for comparison)