Migrating from v5 to v6
Python version requirement raised to 3.13
v6 drops support for Python 3.12. The minimum required version is now Python 3.13.
If you are still on Python 3.12, upgrade your runtime before updating pypaperless to v6.
v6 is also almost a full rewrite of pypaperless. Three things drove it:
- Models were too tightly coupled to the HTTP layer. In v5, every model instance carried a reference to the client and called it directly. That made testing awkward and sharing models between contexts impossible. v6 models are plain data - all I/O goes through services.
- No runtime type safety. v5 used dataclasses with manual dict conversion, so bad API responses would silently produce wrong values. v6 uses Pydantic v2, which validates every response at parse time.
aiohttpgot removed.httpxis modern, has a cleaner sync/async API and a built-in mock transport that makes testing easier.- Model-level CRUD shortcuts removed — replaced by a dispatcher.
doc.update(),doc.delete(), anddraft.save()are gone. Instead, call the operations directly on thePaperlessClient; the dispatcher routes to the correct service automatically:await paperless.update(model),await paperless.delete(model),await paperless.save(draft). See CRUD.
Quick checklist
| # | What to change | Section |
|---|---|---|
| 1 | Replace aiohttp / yarl with httpx |
Dependencies |
| 2 | Rename class Paperless → PaperlessClient; PaperlessConfig → PaperlessSettings |
Initializing the client |
| 3 | Constructor changed: env-var mode → PaperlessClient.from_env(); config mode → PaperlessClient.from_config(cfg) |
Initializing the client |
| 4 | request_api_version removed from constructor and PaperlessSettings |
Initializing the client |
| 5 | Replace reduce() with filter() - different call pattern |
Iteration and filtering |
| 6 | draft() renamed to create(); model shortcuts update(), delete(), save() removed - use service or dispatcher |
CRUD |
| 7 | Replace request_permissions = True with with_permissions() |
Permissions |
| 8 | Rename doc.get_download(), doc.get_metadata(), etc. - shortcuts are back |
Document convenience methods renamed |
| 9 | Note deletion: note.delete() → service call |
Document notes |
| 10 | generate_api_token() is now a module-level function, no longer a static class method |
Token generation |
| 11 | New: profile, trash, documents.history, share_links, documents.bulk_edit, bulk_edit_objects |
New resources |
| 12 | Rename four Paperless-prefixed exception classes; new DeletionError, DispatchError |
Error handling |
Dependencies
Replace aiohttp and yarl with httpx:
Initializing the client
Class renamed
The client class and settings class were renamed:
| v5 | v6 |
|---|---|
Paperless |
PaperlessClient |
PaperlessConfig |
PaperlessSettings |
Update all imports and type annotations accordingly.
Constructor signature changed
In v5, the constructor accepted url, token, and request_api_version. In v6 the constructor only accepts url, token, and client. Environment-variable and config-object modes are now explicit factory class methods.
SSL / TLS customization
Pass a pre-configured httpx.AsyncClient to control TLS behaviour:
The url parameter no longer accepts yarl.URL objects - pass a plain string.
PaperlessSettings and factory class methods
v6 exposes PaperlessSettings (backed by pydantic-settings) and two factory class methods.
Config object - useful when you want to construct or validate settings in one place:
Environment variables - use the from_env() factory; PaperlessSettings reads the values automatically:
| Environment variable | Maps to |
|---|---|
PYPAPERLESS_URL |
URL of the Paperless-ngx instance |
PYPAPERLESS_TOKEN |
API token |
Note
PYPAPERLESS_REQUEST_API_VERSION was removed. API version is now negotiated
automatically from the server's x-api-version response header.
Token generation
generate_api_token() is now a module-level function importable from pypaperless, no longer a static method on PaperlessClient. The optional custom-client argument was also renamed from session to client.
Iteration and filtering
reduce() was replaced by filter(). The key difference: the context manager now yields the service object, so you iterate over ctx instead of reusing the outer service name.
pages() is available and returns enhanced Page objects with .items, .current_page, .last_page, and more.
You can also use the convenience helpers:
docs = await paperless.documents.as_list()
ids = await paperless.documents.all() # list of primary keys only
dmap = await paperless.documents.as_dict() # {pk: Document}
CRUD
In v5, CRUD operations lived on model instances. v6 moves the canonical API to
the service level. Model-level shortcuts (doc.update(), doc.delete(), draft.save()) have been removed.
New in v6: client-level dispatcher
Instead of calling the operation on a specific service, you can call it directly on
the PaperlessClient instance. The dispatcher automatically routes to the correct
service based on the model type — no need to know which service owns the model:
doc = await paperless.documents(42)
doc.title = "New Title"
await paperless.update(doc) # routes to DocumentService.update()
await paperless.delete(doc) # routes to DocumentService.delete()
draft = paperless.tags.create(name="urgent")
pk = await paperless.save(draft) # routes to TagService.save()
This works for all dispatchable resources: documents, correspondents, document types, storage paths, tags, share links, and custom fields.
v6 provides two equivalent ways to perform CRUD:
- Service-level — call the operation on the service that owns the model type.
- Client-level dispatcher — call
await paperless.update(model)/await paperless.delete(model)/await paperless.save(draft)directly on the client; the dispatcher resolves the responsible service automatically.
update()
delete()
delete() no longer returns a boolean. It raises DeletionError on failure
(or swallows it when silent_fail=True).
This applies to every resource - correspondents, tags, custom fields, etc.
save() / create()
draft() was renamed to create(). The model-level draft.save() shortcut was
removed; use the service or the dispatcher instead.
For all other resources (correspondents, tags, …):
Permissions
The mutable request_permissions setter was replaced by a with_permissions() context manager. The flag is now automatically reset on exit.
Note
Unlike v5, with_permissions() resets it automatically on exit, even if an exception occurs.
Document convenience methods renamed
The get_* shortcut methods that existed in v5 on Document instances are fully removed in v6.
All file and metadata access now goes through the service:
| v5 (on model instance) | v6 |
|---|---|
await doc.get_download() |
await paperless.documents.download(doc.id) |
await doc.get_download(original=True) |
await paperless.documents.download(doc.id, original=True) |
await doc.get_preview() |
await paperless.documents.preview(doc.id) |
await doc.get_thumbnail() |
await paperless.documents.thumbnail(doc.id) |
await doc.get_metadata() |
await paperless.documents.metadata(doc.id) |
await doc.get_suggestions() |
await paperless.documents.suggestions(doc.id) |
| (not available) | async for d in paperless.documents.more_like(doc.id): |
| (not available) | await paperless.documents.email(doc.id, ...) |
Sub-service shortcuts for notes, history and share links remain available via bound
sub-service properties on the Document instance:
| Access | Equivalent |
|---|---|
await doc.notes() |
await paperless.documents.notes(doc.id) |
await doc.history() |
await paperless.documents.history(doc.id) |
await doc.share_links() |
await paperless.documents.share_links(doc.id) |
Document notes
Note deletion moved from the model to the service, consistent with the general CRUD pattern. The model-level note.delete() shortcut was removed.
The doc.notes property on Document instances still exposes a bound DocumentNoteService:
Creating a new note:
Note
When using doc.notes, the document pk is bound automatically - no need to pass document= to create().
Note
save() now returns only the new note id as int. In v5 it returned a (note_id, doc_id) tuple.
New resources
Six new services were added.
paperless.profile
Access the currently authenticated user's own profile:
See Profile for details.
paperless.trash
Browse and manage soft-deleted documents:
async for doc in paperless.trash:
print(doc.id, doc.title, doc.deleted_at)
await paperless.trash.restore([42, 43])
await paperless.trash.empty()
The Document.is_deleted property returns True for documents retrieved from the trash.
See Trash for details.
paperless.share_links
Create and manage publicly accessible share links for documents without requiring authentication. Supports full CRUD.
from pypaperless.models.share_links import ShareLinkFileVersion
draft = paperless.share_links.create()
draft.document = 42
draft.file_version = ShareLinkFileVersion.ARCHIVE
slug = await paperless.share_links.save(draft)
print(slug) # "abc123xyz"
link = await paperless.share_links(8)
await paperless.share_links.delete(link)
Document model instances also expose a share_links() shortcut:
See Share Links for details.
paperless.documents.history
A new document audit-log sub-service:
entries = await paperless.documents.history(42)
for entry in entries:
print(entry.actor, entry.timestamp, entry.action)
paperless.documents.bulk_edit
A new sub-service for performing bulk operations across many documents in a single API call. All operations accept a list of document primary keys.
# Assign metadata to multiple documents at once
await paperless.documents.bulk_edit.set_correspondent([1, 2, 3], 5)
await paperless.documents.bulk_edit.set_document_type([1, 2], 3)
await paperless.documents.bulk_edit.set_storage_path([1, 2], 4)
# Tag operations
await paperless.documents.bulk_edit.add_tag([1, 2, 3], 7)
await paperless.documents.bulk_edit.remove_tag([1, 2, 3], 7)
await paperless.documents.bulk_edit.modify_tags([1, 2], add_tags=[5], remove_tags=[2])
# Custom fields
await paperless.documents.bulk_edit.modify_custom_fields(
[1, 2],
add_custom_fields={3: "open"},
remove_custom_fields=[4],
)
# Permissions
from pypaperless.models.types import Permissions
await paperless.documents.bulk_edit.set_permissions(
[1, 2, 3],
owner=1,
permissions=Permissions(view_users=[2, 3], change_users=[1]),
)
# Document operations
await paperless.documents.bulk_edit.delete([10, 11]) # move to trash
await paperless.documents.bulk_edit.reprocess([1, 2, 3]) # re-run OCR
await paperless.documents.bulk_edit.rotate([1, 2], 90)
await paperless.documents.bulk_edit.merge(
[10, 11, 12],
metadata_document_id=10,
delete_originals=True,
)
Raises BulkEditError (a ResponseError subclass) when the API returns a non-OK
result.
paperless.bulk_edit_objects
A new top-level service for setting permissions or deleting multiple non-document objects - tags, correspondents, document types, or storage paths - in a single call.
from pypaperless.models.types import Permissions
# Set permissions on several tags
await paperless.bulk_edit_objects.set_permissions(
"tags",
[1, 2, 3],
owner=1,
permissions=Permissions(view_users=[2], change_users=[1]),
)
# Permanently delete correspondents
await paperless.bulk_edit_objects.delete("correspondents", [4, 5])
Raises BulkEditError when the API returns a non-OK result.
See Bulk Edit Objects for details.
Error handling
PaperlessConnectionError is now raised for httpx.ConnectError rather than aiohttp.ClientConnectorError. If you catch library-specific exceptions, update accordingly:
Tip
PaperlessConnectionError wraps httpx.ConnectError and works in both v5 and v6 - catching it is the most forward-compatible option.
Exception renames
Four exception classes lost their Paperless prefix to follow standard Python naming conventions. PaperlessConnectionError is the only exception kept as-is, because it would otherwise shadow Python's built-in ConnectionError.
| v5 | v6 |
|---|---|
PaperlessAuthError |
AuthError |
PaperlessInvalidTokenError |
InvalidTokenError |
PaperlessInactiveOrDeletedError |
InactiveOrDeletedError |
PaperlessForbiddenError |
ForbiddenError |
New exception base classes
v6 introduces intermediate base classes that you can use to catch whole groups of related errors:
| Class | Catches |
|---|---|
InitializationError |
All session/transport errors (unchanged from v5) |
ResponseError |
BadJsonResponseError, JsonResponseWithError, BulkEditError |
DraftError |
DraftFieldRequiredError, DraftNotSupportedError |
ResourceError |
DeletionError, ItemNotFoundError, PrimaryKeyRequiredError, TaskNotFoundError |
DocumentError |
AsnRequestError, SendEmailError |
New exceptions
| Exception | When raised |
|---|---|
DeletionError |
delete() call receives a non-2xx HTTP response |
DispatchError |
update() / delete() / save() called on an unregistered model type |