carddav
English · 简体中文
A thin, predictable CardDAV client library for Python — the source code of the PyPI package carddav.
Status: 0.1.0. Everything below is implemented and exercised against a real server (Synology Contacts / Radicale).
Features
- Discovery —
.well-known/carddav→current-user-principal→addressbook-home-set→ address books - Read — list resources (
href/etag), batch fetch withaddressbook-multiget, search withaddressbook-query - Incremental sync — RFC 6578
sync-collectionwithsync-token, including the case where the token has expired - Write —
PUTto create (withIf-None-Match: *) or update (withIf-Match),DELETE;POSTkept as a compatibility path for servers that assign URLs - Raw access —
Card.datareturns the exact bytes the server sent; nothing is parsed or rewritten - Pluggable HTTP — one runtime dependency (
httpx);requestsand custom backends selectable, which also makes mocking trivial - Thin CLI —
carddav exportwrites one directory per address book (or one merged.vcf), plus aninfo.jsonindex
What it is not
Deliberately out of scope, because they belong to the caller:
- vCard parsing, field mapping, deduplication, merging, normalization
- a sync engine: no local database, no index, no conflict resolution, no cached state
- UID generation or rewriting
- the Google People API — Google Contacts is reachable two ways: the CardDAV endpoint (what this library speaks) and the People REST API (Google's official Python client is
google-api-python-client). Only the CardDAV path is implemented here; if you need the People API, use that SDK in your own code — this library neither wraps nor depends on it - OAuth2 authorization-code flow and token refresh —
BearerAuth(token)takes a token you already obtained - administering the server over DAV — this library reads and writes cards, it does not manage collections: no
OPTIONS/DAVcapability probing (we discover by doing), no extendedMKCOL, noPROPPATCH, noCOPY/MOVE, noLOCK/UNLOCK(If-Matchcovers the conflicts we care about), and no partial retrieval viaCARDDAV:prop— no server implements it
This is on purpose: a client that "understands" your contacts can no longer tell you what the server actually did.
Other available options
A handful of other projects touch the CardDAV space. None of them is a small, dependency-light Python library that talks CardDAV directly and leaves your contact data alone — the gap this project fills.
| Project | Description | Why it does not fit here |
|---|---|---|
caldav |
A CalDAV client library for Python (calendars and tasks). | Built around CalDAV, not address books; its methods and data model assume VEVENT/VTODO, so CardDAV contacts are out of scope. |
pyCardDAV |
A Python 2-era standalone contacts client with a CLI. | Abandoned and Python 2 only; not importable as a library, and unmaintained for years. |
carddavclient |
A CardDAV client written in PHP. | Solves the right problem but in PHP, so it cannot be a dependency of a Python project. |
vdirsyncer |
A sync engine that can talk to CardDAV servers. | A full application, not an installable library; its CardDAV code is internal and not exposed for reuse. |
Installation
pip install carddav # httpx backend (default)
pip install "carddav[requests]" # optional: requests backend
Requires Python 3.10+. Note that Python 3.10 is end-of-life upstream, so it is supported on a best-effort basis and the floor will move up together with our dependencies.
Authentication
| Method | How | Notes |
|---|---|---|
| Basic | get_davclient(url, username=..., password=...) |
used automatically when username is given |
| Digest | auth=DigestAuth(user, password) |
must be passed explicitly — there is no automatic 401-challenge handling |
| Bearer | auth=BearerAuth(token) |
for OAuth2-protected endpoints; you obtain and refresh the token |
| Your own | auth=<backend-native auth object> |
wins over username / password; it must match the chosen backend (e.g. httpx.Auth) |
Not included: OAuth2 authorization-code flow and token refresh, client certificates (mTLS), NTLM / Kerberos.
HTTP backends
The HTTP stack sits behind a two-method interface (Backend: request() / close()), so it can be swapped or mocked without touching the library:
get_davclient(url, backend="httpx") # default
get_davclient(url, backend="requests") # pip install "carddav[requests]"
get_davclient(url, backend=my_fake) # any object with request()/close()
The same seam is what makes offline tests possible — see tests/test_backend.py.
Server URL
The url you pass to get_davclient can point at any level of the CardDAV
hierarchy — the library discovers the address book(s) from there. Coarser URLs
make the library walk more discovery steps; a direct address-book URL skips
discovery entirely. All of them return the same AddressBook / Card objects.
| Granularity | What it is | Example |
|---|---|---|
| Bare host | Just the domain. The library follows RFC 6764 and looks up /.well-known/carddav to find the entry point. |
https://cloud.example.org |
| Principal URL | The authenticated user's own root, i.e. the current-user-principal. |
https://cloud.example.org/remote.php/dav/principals/users/me/ |
| Addressbook-home-set URL | The container that holds all of the user's address books. A server may place it on a different host — iCloud returns a shard host, which is normal, not an error. | https://p48-contacts.icloud.com/1437425399/carddavhome/ |
| A specific address book URL | One exact address book — discovery is skipped. | https://p48-contacts.icloud.com/1437425399/carddavhome/card/ |
Discovery itself (well-known → current-user-principal → addressbook-home-set
→ address books) is done by the library; you never write PROPFIND or parse
href paths yourself. What is implemented, and what is deliberately not, is listed in Server support & quirks.
Quick start
The API mirrors the shape of caldav (get_* / put_card / delete_card), so the two libraries feel the same.
from carddav import get_davclient
with get_davclient(
url="https://cloud.example.org",
username="me",
password="app-password", # iCloud needs an app-specific password
) as client: # construction only sets up the connection; get_principal() triggers discovery
book = client.get_principal().get_addressbooks()[0]
cards_meta = book.get_cards() # metadata only: href / etag (uid needs the body)
for meta in cards_meta:
print(meta.href, meta.etag)
card = book.get_card(cards_meta[0].href) # raw bytes + etag
print(len(card.data or b"")) # exactly what the server sent
etag = book.put_card(new_vcf, href=card.href, if_match=card.etag) # 412 if changed meanwhile
book.delete_card(card.href, if_match=etag)
etag = book.put_card(vcf_bytes, create_only=True) # PUT <uid>.vcf, refuse if it exists
href = book.post_card(vcf_bytes) # only for servers that assign URLs (quirk path)
changes, token = book.get_changes() # first run: full listing
changes, token = book.get_changes(sync_token=token) # afterwards: only changes
for change in changes:
print(change.href, change.deleted)
cards = book.search(query_xml, depth="1") # addressbook-query, sent verbatim
Examples
Runnable scripts live in examples/. They read
CARDDAV_URL / CARDDAV_USER / CARDDAV_PASSWORD from the environment, so they need no
arguments:
export CARDDAV_URL=https://cloud.example.org CARDDAV_USER=me CARDDAV_PASSWORD='***'
uv run python examples/list_books.py # address books and card counts
uv run python examples/export_book.py --out ./export
uv run python examples/watch_changes.py # keeps a sync token in a local file
uv run python examples/search_cards.py --text Alice
Command line
A thin carddav command ships with the package. Its only implemented subcommand today
is export; it does no field judgement, it just discovers, fetches and writes.
# one directory per address book, one .vcf per card (the default)
carddav export --url https://cloud.example.org \
--user me --password '***' --out-dir ./export
# one merged .vcf per address book instead
carddav export --url https://cloud.example.org --user me --out-dir ./export --single-file
# look before you write
carddav export --url https://cloud.example.org --user me --out-dir ./export --dry-run
--url accepts any of the four entry shapes (bare host, principal, address-book
home-set, or a single collection). Each exported name is
"<displayname>(<url segment>)" — the segment is the identity, the display name is
only a readable prefix. Both layouts write an info.json recording every address
book's href and every card's href/etag/uid. Existing files are skipped and
reported (exit code 1) unless you pass --overwrite; nothing is ever deleted. Details and the exact
file layout and info.json schema: docs/cli.md.
Server support
| Server | Auth | Status |
|---|---|---|
| Radicale — incl. Synology Contacts | Basic | tested 2026-10-29 |
| Nextcloud / ownCloud (sabre/dav), incl. Disroot | Basic | not yet tested |
| Baikal (sabre/dav) | Basic | not yet tested |
| iCloud | app-specific password | not yet tested |
| Google Contacts (CardDAV endpoint, not the People API) | OAuth 2.0 access token supplied by caller | not yet tested (no OAuth flow in the library) |
| Fastmail | app password | not yet tested |
"Status" means verified by our own live tests, not "the protocol should allow it". Radicale/Synology is the reference server today; sabre/dav, iCloud and Google still need credentials. Per-server quirks and the practical rules that follow are in docs/quirks.md.
Documentation
| Page | What it covers |
|---|---|
| Server support & quirks | Which servers are verified, what each one does differently, and the practical rules that follow |
| Sync model | Why there is no sync engine, and what your application must store |
| CLI reference | carddav export: output layout, naming, info.json, exit codes |
| Changelog | Release history |
Every page except the changelog exists in both English and Chinese (*.zh-CN.md).
Contributing
Thanks for taking a look. The scope is deliberately narrow, so the rules below are short — but the ones marked non-negotiable are enforced in review.
Before you start
- Bug reports / questions → open an issue. Please include the server software and version (
Nextcloud 30,iCloud, …), since CardDAV behaviour differs a lot between implementations. - Anything bigger than a typo fix → open an issue first. API shape and protocol handling are still being decided, and a PR that arrives before the design does usually has to be rewritten.
- Security issues → do not open an issue; use GitHub Security Advisories.
- README changes →
README.mdandREADME.zh-CN.mdare mirrors; update both in the same PR.
Development setup
Python 3.10+. Everything runs through uv:
uv sync # create the venv and install dev dependencies
uv run pytest # full test suite (offline, no credentials needed)
uv run ruff check . # lint
uv run ruff format --check .
uv run mypy src
The test suite must stay fully offline: no real CardDAV server, no credentials, no environment variables. Real servers are only used for manual smoke tests.
Non-negotiable rules
These exist because the library has two jobs a "helpful" client would break: report what the server actually did, and stay small.
- No real contact data, ever. No names, phone numbers, e-mail addresses, or
hrefs containing a username — not in tests, fixtures, docstrings or commit messages. - No credentials, ever. No passwords, app-specific passwords, tokens, or
.envcontents. - Do not parse vCard.
Card.datamust be the bytes the server returned.Card.uidmay only scan for theUIDline (handling folded lines). - No field logic. Comparison keys, deduplication, merging and normalization belong to the calling application.
- Runtime dependencies:
httpxonly.requests(optional backend) is the only extra. A new dependency needs a written justification first. - Writes are never retried automatically, and UIDs are never generated or rewritten implicitly.
- All HTTP goes through
Transport.request(); only_backend_httpx.py/_backend_requests.pymay import an HTTP library. - Reject XML containing
<!DOCTYPEbefore parsing. - Code comments and docstrings are English only (no Chinese or other non-Latin scripts). Non-ASCII European characters such as
§(section sign),ß,éare allowed — strict ASCII is not required. Identifiers (function/variable/public-API names) stay English (a technical necessity). Public pages underdocs/are bilingual; developer notes underinternal/are Chinese.
Tests
- Every protocol behaviour needs a test — including failure paths (
401,403,412,507, expiredsync-token). - If you change a request body, assert the exact bytes sent. Request bodies are string templates for exactly this reason.
- A bug fix should come with a test that fails before the fix.
Pull requests
- One logical change per PR; keep refactors separate from behaviour changes.
- Update the relevant page: user-visible facts →
docs/(English + Chinese); design decisions → the repository'sinternal/notes. - Commit messages: short imperative subject, then why in the body if it is not obvious.
By contributing you agree that your contribution is licensed under Apache-2.0, the license of this project.
⚠️ Never paste real contact data or credentials into an issue. Redact names, phone numbers, e-mail addresses and any
hrefcontaining your username.
Security
Please report vulnerabilities privately via GitHub Security Advisories instead of a public issue. Credential leakage and XML parsing issues are treated as security bugs.
Versioning
This project follows Semantic Versioning. While the version is 0.x, breaking changes are allowed but always bump the minor version and are documented in the CHANGELOG; patch releases never change the public API or raise a dependency floor.
Raising requires-python or a runtime dependency floor is treated as a breaking change — it can make the library uninstallable for you.
License
Apache License 2.0 — see LICENSE and NOTICE.
Related projects
- A companion tool for field research, identity matching and address-book merging (private, by the same author) consumes this library. It is not required: this library never depends on it.
Metadata
Release files for carddav 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| carddav-0.1.0.tar.gz | 43.8 kB | Without provenance | Details for carddav-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| carddav-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for carddav-0.1.0-py3-none-any.whl |
Total release size: 88.5 kB
Release files / carddav-0.1.0.tar.gz
| Download URL | carddav-0.1.0.tar.gz |
|---|---|
| Size | 43.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4735df6f2440177d786a6ef25b53fe1d7e786415974dd2989b452dda2b92715
|
|
BLAKE2b-256 checksum How to use checksums |
c8c80be562795b4a3884bfdf87334df3531926f14d1fc5fc807546a2c63240d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / carddav-0.1.0-py3-none-any.whl
| Download URL | carddav-0.1.0-py3-none-any.whl |
|---|---|
| Size | 44.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1872e1d1dd54e8dd856a1fb1b31677c306f9632e3ba88350cc9f499b71dd9ea3
|
|
BLAKE2b-256 checksum How to use checksums |
223abff3c05fa172b1f30654a421ea8939b4133bbae3ad367e3a6efb9bf83b37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for carddav 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.