PyAppleBooks
PyAppleBooks is a Python API library to access your Apple Books data.
Installation
pip install py_apple_books
Requires Python 3.10+ on macOS.
Available Functions
Collections
| Function | Description | Parameters | Return Type |
|---|---|---|---|
list_collections() |
List all collections | limit?, order_by? |
ModelIterable |
get_collection_by_id(collection_id) |
Get a collection by its ID | collection_id: str |
Collection |
get_collection_by_title(title) |
Search collections by title substring | title: str |
ModelIterable |
Collection writes (v1.9.0+)
Apple exposes no automation API for collections, so these write directly to the
library's SQLite store — behind guard rails: every call refuses while Books.app
is running, takes a WAL-inclusive timestamped backup (kept under
~/.py_apple_books/backups/, restorable via write_safety.restore_library),
validates the schema and aborts on drift, and runs in a single transaction that
maintains Core Data's bookkeeping. Only user-created collections can be renamed
or deleted; membership edits also work on "Want to Read". Writes also refuse
when Books.app's state can't be verified — off macOS (e.g. in Docker), or if
pgrep fails — raising WriteError.
⚠️ With iCloud "Collections, bookmarks and highlights" sync enabled, direct writes may not propagate to other devices and can be reverted by a cloud re-sync. Verify a small edit cross-device before relying on it.
| Function | Description | Parameters | Return Type |
|---|---|---|---|
create_collection(title, details?) |
Create a user collection | title: str, details?: str |
Collection |
rename_collection(collection_id, new_title) |
Rename a user collection | collection_id, new_title: str |
Collection |
delete_collection(collection_id) |
Soft-delete a user collection (books untouched) | collection_id |
None |
add_book_to_collection(collection_id, book_id) |
Add a book (idempotent) | collection_id, book_id |
bool |
remove_book_from_collection(collection_id, book_id) |
Remove a book (idempotent) | collection_id, book_id |
bool |
Books
| Function | Description | Parameters | Return Type |
|---|---|---|---|
list_books() |
List all books | limit?, order_by? |
ModelIterable |
get_book_by_id(book_id) |
Get a book by its ID | book_id: str |
Book |
get_book_by_title(title) |
Search books by title substring | title: str |
ModelIterable |
get_books_by_genre(genre) |
Search books by genre substring | genre: str, limit?, order_by? |
ModelIterable |
Reading Progress
| Function | Description | Parameters | Return Type |
|---|---|---|---|
get_books_in_progress() |
Books currently being read (0% < progress < 100%) | limit?, order_by? |
ModelIterable |
get_finished_books() |
Books marked as finished | limit?, order_by? |
ModelIterable |
get_unstarted_books() |
Books not yet started | limit?, order_by? |
ModelIterable |
get_recently_read_books() |
Books ordered by last-opened date, most recent first | limit? (default 10) |
ModelIterable |
Annotations
| Function | Description | Parameters | Return Type |
|---|---|---|---|
list_annotations() |
List all annotations | limit?, order_by? |
ModelIterable |
get_annotation_by_id(annotation_id) |
Get an annotation by its ID | annotation_id: str |
Annotation |
get_annotations_by_color(color) |
Filter annotations by highlight color | color: str, limit?, order_by? |
ModelIterable |
search_annotation_by_highlighted_text(text) |
Substring search in highlighted text | text: str, limit?, order_by? |
ModelIterable |
search_annotation_by_note(note) |
Substring search in user notes | note: str, limit?, order_by? |
ModelIterable |
search_annotation_by_text(text) |
Substring search across highlighted text, representative text, and notes | text: str, limit?, order_by? |
ModelIterable |
get_annotations_by_date_range(after?, before?) |
Filter annotations by creation date | after?: datetime, before?: datetime, limit?, order_by? |
ModelIterable |
Book Content (v1.7.0+)
Read the full text of your non-DRM EPUBs. Powered by ebooklib and beautifulsoup4.
| Function | Description | Parameters | Return Type |
|---|---|---|---|
get_book_content(book_id) |
Return a BookContent handle after verifying the book is downloaded and not DRM-protected |
book_id: int |
BookContent |
get_current_reading_location(book_id) |
Apple Books' auto-tracked "current reading position" bookmark (a zero-width annotation with a CFI) | book_id: int |
Optional[Annotation] |
get_current_reading_chapter(book_id) |
Convenience: resolve the bookmark's CFI to a Chapter |
book_id: int |
Optional[Chapter] |
BookContent methods:
| Method | Description | Return Type |
|---|---|---|
list_chapters() |
Flattened table of contents with title, href, fragment, order, depth | list[Chapter] |
get_chapter_content(chapter_id) |
Plain text of a chapter, scoped to its fragment anchor | str |
chapter_at_cfi(cfi) |
Resolve an EPUB CFI to the containing Chapter |
Optional[Chapter] |
BookContent properties:
| Property | Description |
|---|---|
is_epub |
True if the path is an EPUB bundle directory |
is_pdf |
True if the path is a single PDF file |
is_downloaded |
True if locally materialized (not an iCloud placeholder) — does not trigger hydration |
is_drm_protected |
True if the EPUB carries FairPlay META-INF/sinf.xml, Adobe rights.xml, or an encryption.xml that encrypts more than fonts (font obfuscation alone doesn't count; an unparseable file does) |
Exceptions
Content-access methods may raise:
| Exception | When |
|---|---|
BookNotDownloadedError |
The book has no local file (never downloaded) or the file is an iCloud placeholder |
DRMProtectedError |
The book is DRM-protected — usually an Apple Books Store purchase (FairPlay), occasionally an encrypted imported EPUB |
UnsafeEpubEntryError |
A file in the EPUB bundle resolves outside it (absolute/../ href, symlink), isn't a regular file, or is implausibly large; the book is refused |
AppleBooksError |
Base class for all exceptions above, plus EPUB-parsing errors |
Examples
Creating a client
from py_apple_books import PyAppleBooks
api = PyAppleBooks()
List books
for book in api.list_books():
print(f"{book.title} — {book.author or 'Unknown Author'}")
Get annotations
for a in api.list_annotations(limit=5):
print(f"[{a.color}] {a.selected_text}")
Search highlights
for a in api.search_annotation_by_highlighted_text('genome'):
print(a.selected_text)
Filter by color
for a in api.get_annotations_by_color('yellow'):
print(a.selected_text)
Reading progress
for book in api.get_books_in_progress(limit=5):
print(f"{book.title}: {book.reading_progress:.1f}%")
Read chapter content
from py_apple_books.exceptions import BookNotDownloadedError, DRMProtectedError
book = api.get_book_by_id(42)
try:
content = api.get_book_content(book.id)
except BookNotDownloadedError:
print("Open the book in Apple Books first to download it.")
except DRMProtectedError:
print("DRM-protected Store purchase — text content isn't readable.")
else:
for ch in content.list_chapters():
print(f"{' ' * ch.depth}[{ch.order}] {ch.title}")
# Read the first substantive chapter
text = content.get_chapter_content(content.list_chapters()[0].id)
print(text[:500])
See what the user is currently reading
# The book they most recently opened
for book in api.get_recently_read_books(limit=1):
ch = api.get_current_reading_chapter(book.id)
if ch is None:
continue
content = api.get_book_content(book.id)
text = content.get_chapter_content(ch.id)
print(f"Currently reading: {book.title}")
print(f"Progress: {book.reading_progress:.1f}%")
print(f"Chapter {ch.order}: {ch.title}")
print(text[:500])
Get all collections and books
for collection in api.list_collections():
print(f"{collection.title}: {len(collection.books)} books")
for book in collection.books:
print(f" - {book.title}")
How content access handles Apple Books' quirks
- iCloud placeholders — books you've imported but haven't opened lately can live only in iCloud.
is_downloadeddetects this viaos.stat(for files) ordu -sk(for bundle directories) without triggering a download.get_book_contentraisesBookNotDownloadedErrorso you can prompt the user to open the book in Apple Books. - DRM'd Store purchases — FairPlay-encrypted EPUBs carry
META-INF/sinf.xml/ anencryption.xmlcovering their content, and their chapter bodies are opaque ciphertext. Detected up-front from that evidence; callers get a clearDRMProtectedError. EPUBs whoseencryption.xmlonly obfuscates fonts are readable. - Untrusted book files — imported EPUBs are stored as unzipped bundles, so every entry is read through a contained reader: anything resolving outside the bundle, non-regular files and oversized entries raise
UnsafeEpubEntryErrorinstead of exposing unrelated local files. - Missing metadata — Apple's unknown-author placeholder is returned as
author=None, and theZPAGECOUNTplaceholder (0 or 1) aspage_count=None. - Non-standard EPUB layouts — OPFs at unusual paths, NCX files not declared in
<spine toc=…>(e.g. The 4-Hour Workweek), EPUB3 books with nav docs only (e.g. On Numbers and Games), URL-encoded href characters (%21→!), duplicate navPoint ids — all handled, with a stdlib NCX override for the ebooklib blind spots. - Fragment-scoped chapters — Project Gutenberg EPUBs often put multiple sections in one XHTML file, separated by anchors.
get_chapter_contentreturns only the requested section, not the whole file.
Running the tests
pip install -e '.[dev]'
pytest
The test suite covers placeholder/DRM detection, NCX parsing, XHTML text extraction, fragment scoping, and end-to-end BookContent behavior against generated EPUB fixtures. No real Apple Books library required.
Upcoming Features
- Adding a book to collection
- Removing a book from collection
- Updating annotations
Contribution
Thank you for considering contributing to this project! Your help is greatly appreciated.
Opening Issues
If you encounter a bug, have a feature request, or want to discuss something related to the project, please open an issue on the GitHub repository. When opening an issue, please provide:
Bug Reports: Describe the issue in detail. Include steps to reproduce the bug if possible, along with any error messages or screenshots.
Feature Requests: Clearly explain the new feature you'd like to see added to the project. Provide context on why this feature would be beneficial.
General Discussions: Feel free to start discussions on broader topics related to the project.
Contributing
1️⃣ Fork the GitHub repository https://github.com/vgnshiyer/py-apple-books
2️⃣ Create a new branch for your changes (git checkout -b feature/my-new-feature).
3️⃣ Make your changes and test them thoroughly.
4️⃣ Push your changes and open a Pull Request to main.
Please provide a clear title and description of your changes.
License
PyAppleBooks is licensed under the MIT license. See the LICENSE file for details.
Metadata
Release files for py-apple-books 1.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| py_apple_books-1.9.1.tar.gz | 73.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_apple_books-1.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 126.0 kB
Release files / py_apple_books-1.9.1.tar.gz
| Download URL | py_apple_books-1.9.1.tar.gz |
|---|---|
| Size | 73.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a689d5c65d444e21c7654383e364e57850fa652c11d446ab797eab6a885d81f2
|
|
BLAKE2b-256 checksum How to use checksums |
f17c8f01e8f106c7b8253cc098c6bbedb89dd0e1c4259d566d3a08bf56d4d5a4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / py_apple_books-1.9.1-py3-none-any.whl
| Download URL | py_apple_books-1.9.1-py3-none-any.whl |
|---|---|
| Size | 52.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c733bac24e8039398791ec39551ce258a25b739c8e6d5a4f7d63528433f6c36
|
|
BLAKE2b-256 checksum How to use checksums |
2aa821fa4bf2e9d7b177f82f7e26a64729b0e3b3d91c123387940ebcc4732960
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|