In this tutorial you should learn basic usage of the python CalDAV
client library. You are encouraged to copy the code examples into a
file and add a breakpoint() inside the with-block so you can
inspect the return objects you get from the library calls. Do not
name your file caldav.py or calendar.py, this may break some
imports.
To follow this tutorial as intended, each code block should be run towards a clean-slate Radicale server. To do this, you need:
- The source code of caldav with tests:
git clone https://github.com/python-caldav/caldav.git ; cd caldav - The Radicale python package:
pip install radicale - An environmental variable set:
export PYTHON_CALDAV_USE_TEST_SERVER=1
With this setup, the with-blocks in the code sections below will spin up a Radicale server.
When you've run the tutorial as intended, I recommend going through the examples again towards your own calendar server:
- Set the environment variables
CALDAV_URL,CALDAV_USERandCALDAV_PASSWORDto point to your personal calendar server. - Be aware that different calendar servers may behave differently. For instance, not all of them allows you to create a calendar. Some are even read-only.
- You will need to revert all changes done. The code examples below does not do any cleanup. If your calendar server supports creating and deleting calendars, then it should be easy enough:
`my_new_calendar.delete()`inside the with-block. Events also has a.delete()-method. Beware that there is noundo. You're adviced to have a local backup of your calendars. I'll probably write a HOWTO on that one day. - Usage of a context manager is considered best practice, but not really needed - you may skip the with-statement and write just
client = get_davclient(). This will make it easier to test code from the python shell.
As of 2.0, it's recommended to start initiating a
:class:`caldav.davclient.DAVClient` object using the get_davclient
function, go from there to get a
:class:`caldav.collection.Principal`-object, and from there find a
:class:`caldav.collection.Calendar`-object. (I'm planning to add a
get_calendar in version 3.0). This is how to do it:
from caldav.davclient import get_davclient
from caldav.lib.error import NotFoundError
with get_davclient() as client:
my_principal = client.principal()
try:
my_calendar = my_principal.calendar()
print(f"A calendar was found at URL {my_calendar.url}")
except NotFoundError:
print("You don't seem to have any calendars")Caveat: Things will break if password/url/username is wrong, but
perhaps not where you expect it to. To test, you may try out
get_davclient(username='alice', password='hunter2',url='https://calendar.example.com/dav/').
The calendar-method above gives one calendar - if you have more
calendars, it will give you the first one it can find - which may not
be the correct one. To filter there are parameters name and
cal_id - I recommend testing them:
from caldav.davclient import get_davclient
from caldav.lib.error import NotFoundError
with get_davclient() as client:
my_principal = client.principal()
try:
my_calendar = my_principal.calendar(name="My Calendar")
except NotFoundError:
print("You don't seem to have a calendar named 'My Calendar'")If you happen to know the URL or path for the calendar, you don't need to go through the principal object.
from caldav.davclient import get_davclient
with get_davclient() as client:
my_calendar = client.calendar(url="/dav/calendars/mycalendar")Note that in the example above, no communication is done. If the URL is wrong, you will only know it when trying to save or get objects from the server!
For servers that supports it, it may be useful to create a dedicated test calendar - that way you can test freely without risking to mess up your calendar events. Let's populate it with an event while we're at it:
from caldav.davclient import get_davclient
import datetime
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(name="Test calendar")
may17 = my_new_calendar.save_event(
dtstart=datetime.datetime(2020,5,17,8),
dtend=datetime.datetime(2020,5,18,1),
uid="may17",
summary="Do the needful",
rrule={'FREQ': 'YEARLY'})You have icalendar code and want to put it into the calendar? Easy!
from caldav.davclient import get_davclient
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(name="Test calendar")
may17 = my_new_calendar.save_event("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
DTSTART:20200517T060000Z
DTEND:20200517T230000Z
RRULE:FREQ=YEARLY
SUMMARY:Do the needful
END:VEVENT
END:VCALENDAR
""")The best way of getting information out from the calendar is to use the search. Currently most of the logic is done on the server side - and the different calendar servers tends to give different results given the same data and search query. In future versions of the CalDAV library the intention is to do more workarounds and logic on the client side, allowing for more consistent results across different servers.
from caldav.davclient import get_davclient
from datetime import date
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(name="Test calendar")
my_new_calendar.save_event(
dtstart=datetime.datetime(2023,5,17,8),
dtend=datetime.datetime(2023,5,18,1),
uid="may17",
summary="Do the needful",
rrule={'FREQ': 'YEARLY'})
my_events = my_new_calendar.search(
event=True,
start=date(2026,5,1),
end=date(2026,6,1),
expand=True)
assert len(my_events) == 1
print(my_events[0].data)expand matters for recurring events and tasks, instead of getting returned the original event (with DTSTART set in 2023 and an RRULE set) it will return the recurrence for year 2026. Or, rather, a list of recurrences if there are more of them in the search interval.
event causes the search to only return events. There are three kind of objects that can be saved to a calendar (but not all servers support all three) - events, journals and tasks (VEVENT, VJOURNAL and VTODO). This is called Calendar Object Resources in the RFC. Now that's quite a mouthful! To ease things, the word "event" is simply used in documentation and communication. So when reading "event", be aware that it actually means "a CalenderObjectResource objects such as an event, but it could also be a task or a journal" - and if you contribute code, remember to use CalendarObjectResource rather than Event.
Without event=True explicitly set, all kind of objects should be returned. Unfortunately many servers returns nothing - so as of 2.0, it's important to always specify if you want events, tasks or journals. In future versions of CalDAV there will be workarounds for this so event=True can be safely skipped, regardless what server is used.
The return type is a list of objects of the type :class:`caldav.calendarobjectresource.Event` - for tasks and jornals there are similar classes Todo and Journal.
The data property delivers the icalendar data as a string. It can be modified:
from caldav.davclient import get_davclient
from datetime import date
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(name="Test calendar")
my_new_calendar.save_event(
dtstart=datetime.datetime(2023,5,17,8),
dtend=datetime.datetime(2023,5,18,1),
uid="may17",
summary="Do the needful",
rrule={'FREQ': 'YEARLY'})
my_events = my_new_calendar.search(
start=date(2026,5,1),
end=date(2026,6,1),
expand=True)
assert len(my_events) == 1
my_events[0].data = my_events[0].data.replace("Do the needful", "Have fun!")
my_events[0].save()As seen above, we can use save() to send a modified object back to
the server. In the case above, we've edited a recurrence. Now that
we've saved the object, you're encouraged to test with search with and
without expand set and with different years, print out
my_event[0].data and see what results you'll get. The
save()-method also takes a parameter all_recurrences=True if
you want to edit the full series!
The code above is far from "best practice". You should not try to
parse or modify event.data - it will be parsed and thrown into an
object accessible as myevent.icalendar_instance. (in 3.0,
probably myevent.instance will work out without yielding a
DeprecationWarning).
Most of the time every event one gets out from the search contains one
component - and it will always be like that when using
expand=True. To ease things out for users of the library that
wants easy access to the event data there is an
my_events[9].icalendar_component property. From 2.0 also
accessible simply as my_events[0].component:
from caldav.davclient import get_davclient
from datetime import date
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(name="Test calendar")
my_new_calendar.save_event(
dtstart=datetime.datetime(2023,5,17,8),
dtend=datetime.datetime(2023,5,18,1),
uid="may17",
summary="Do the needful",
rrule={'FREQ': 'YEARLY'})
my_events = my_new_calendar.search(
start=date(2026,5,1),
end=date(2026,6,1),
expand=True)
assert len(my_events) == 1
print(f"Event starts at {my_events[0].component.start}")
my_events[0].component['summary'] = "Norwegian national day celebrations"
my_events[0].save()There is a danger to this - there is one (and only one) exception when an event contains more than one component. If you've been observant and followed all the steps in this tutorial very carefully, you should have spotted it.
How to do operations on components and instances in the vobject and icalendar library is outside the scope of this tutorial - The icalendar library documentaiton can be found here as of 2025-06.
Usually tasks and journals can be applied directly to the same calendar as the events - but some implementations (notably Zimbra) has "task lists" and "calendars" as distinct entities. To create a task list, there is a parameter supported_calendar_component_set that can be set to ['VTODO']. Here is a quick example that features a task:
from caldav.davclient import get_davclient
from datetime import date
with get_davclient() as client:
my_principal = client.principal()
my_new_calendar = my_principal.make_calendar(
name="Test calendar", supported_calendar_component_set=['VTODO'])
my_new_calendar.save_todo(
summary="prepare for the Norwegian national day", due=date(2025,5,16))
my_tasks = my_new_calendar.search(
todo=True)
assert len(my_tasks) == 1
my_tasks[0].complete()
my_tasks = my_new_calendar.search(
todo=True)
assert len(my_tasks) == 0
my_tasks = my_new_calendar.search(
todo=True, include_completed=True)
assert len(my_tasks) == 1There are more functionality, but if you've followed the tutorial to this point, you should already know eough to deal with the very most use-cases.
There are some more examples in the examples folder, particularly basic examples. There is also a scheduling examples for sending, receiving and replying to invites, though this is not very well-tested so far. The example code is currently not tested nor maintained. Some of it will be moved into the documentation as tutorials or how-tos eventually.
The test code also covers most of the features available, though it's not much optimized for readability (at least not as of 2025-05).
Tobias Brox is also working on a command line interface built around the caldav library.