# pypaperless v6 - Und wieder ein Reboot

> pypaperless v6 erscheint kurz nach Paperless-ngx v3 und bringt Breaking Changes. Ein Blick hinter die Kulissen.

_Quelle: https://tbsch.de/post/2026-04-01-pypaperless-v6-und-wieder-ein-reboot/ · Stand: 2026-04-01_


Kurz nach dem Release von *Paperless-ngx* v3 folgt *pypaperless* v6 - und diesmal hat es wirklich Konsequenzen. Aufgrund vielerlei Veränderungen in der *Paperless-ngx* REST API und meinem Unwillen, bis in alle Ewigkeit rückwärts-kompatibel zu sein, traf ich eine Entscheidung. Es gibt Breaking Changes. Und damit meine ich nicht „eine Methode wurde umbenannt“, sondern einen tiefgreifenden Umbau der gesamten Bibliothek.

> [!info]
> Einen Überblick über pypaperless, Installation und alle Versionen findest du in der [pypaperless-Übersicht](/tags/pypaperless/).

<div class="tb-gh-card"><div class="tb-gh-og-gate" data-name="consent-gate-github"><a class="tb-gh-og-link" href="https://github.com/tb1337/paperless-api" target="_blank" rel="noopener noreferrer" aria-label="tb1337/paperless-api auf GitHub">
        <img class="tb-gh-og" data-name="consent-gate-github"
          data-src="https://opengraph.githubassets.com/0/tb1337/paperless-api"
          alt="tb1337/paperless-api auf GitHub" loading="lazy" decoding="async" width="1200" height="600" referrerpolicy="no-referrer">
      </a>
    </div>
    <a class="tb-gh-link" href="https://github.com/tb1337/paperless-api" target="_blank" rel="noopener noreferrer">
      <svg aria-hidden="true"
  xmlns="http://www.w3.org/2000/svg"
 
 
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
 
>
  <path stroke="none" d="M0 0h24v24H0z" fill="none" />
  <path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg><span>tb1337/paperless-api auf GitHub besuchen</span>
    </a>
  </div>

## Warum ein Reboot?

Wenn ich ehrlich bin, hatte *pypaperless* in v5 drei Baustellen, über die ich schon länger immer wieder gestolpert bin. Alle drei haben mich genug gestört, um diesmal wirklich von Grund auf neu zu bauen. Und in diesem Anlauf habe ich mein aktuelles Verständnis von SDK-Gedanken noch mehr einfließen lassen.

### 1. Models waren zu eng ans HTTP-Layer gekoppelt

In v5 hielt jede Model-Instanz eine Referenz auf den Client und rief ihn intern direkt auf. Das klingt praktisch, führt aber dazu, dass man Models nicht isoliert testen kann und sie auch nicht ohne Kontext weitergeben kann. Das war beim Testen regelmäßig ein Ärgernis.

In v6 sind Models reine Daten - kein Client, keine HTTP-Aufrufe. Der gesamte I/O läuft ausschließlich über Services. Das macht den Code deutlich sauberer und testbarer. **Und als kleines Schmankerl** habe ich einen kleinen Dispatcher in die `PaperlessClient` Klasse eingebaut, über den wichtige CRUD-Methoden direkt nutzbar sind.

<div class="tb-tab__container" data-tb-component="tabs">
  <div class="tb-tab__nav" role="tablist" aria-label="Tabs"><button class="tb-tab__button tb-tab--active" type="button" role="tab"
        id="tabs-2-tab-0" aria-controls="tabs-2-panel-0"
        aria-selected="true" tabindex="0" data-tab-index="0">
        Vorher (v5)
      </button><button class="tb-tab__button " type="button" role="tab"
        id="tabs-2-tab-1" aria-controls="tabs-2-panel-1"
        aria-selected="false" tabindex="-1" data-tab-index="1">
        Nachher (v6)
      </button></div>
  <div class="tb-tab__content"><div class="tb-tab__panel tb-tab--active" role="tabpanel" tabindex="0"
        id="tabs-2-panel-0" aria-labelledby="tabs-2-tab-0" data-tab-index="0"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">paperless</span><span class="o">.</span><span class="n">documents</span><span class="p">(</span><span class="mi">4711</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="s2">&#34;Ein neuer Titel&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="n">doc</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
</span></span></code></pre></div></div><div class="tb-tab__panel " role="tabpanel" tabindex="0"
        id="tabs-2-panel-1" aria-labelledby="tabs-2-tab-1" data-tab-index="1"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">paperless</span><span class="o">.</span><span class="n">documents</span><span class="p">(</span><span class="mi">4711</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="s2">&#34;Ein neuer Titel&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="n">paperless</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
</span></span></code></pre></div></div></div>
</div>


### 2. Keine Typprüfung zur Laufzeit

v5 nutzte Dataclasses mit manueller Dict-Konvertierung. Wenn die API unerwartet etwas zurückgab, konnte das jedoch still und heimlich zu falschen Werten führen. Es gab zwar eine Warning in der Konsole, aber das war's :grin:.

v6 setzt auf **Pydantic v2**: Jede API-Antwort wird beim Parsen validiert. Ich muss keinem Python-Entwickler die Vorteile davon erklären.

### 3. aiohttp raus, httpx rein

`aiohttp` hat seinen Job getan, aber `httpx` ist moderner, hat eine sauberere API für synchrone und asynchrone Nutzung, und bringt ein eingebautes Mock-Transport-System mit, das das Schreiben von Tests erheblich vereinfacht. Ein Wechsel, der sich auch beim Entwickeln sofort bemerkbar macht.

Als User von *pypaperless* solltest du diesen Change in den meisten Fällen gar nicht bemerken.

## Was ändert sich konkret?

Die vollständige Checkliste der Breaking Changes gibt es im [Migrationsleitfaden](https://pypaperless.docs.tbsch.de/migrating-v5-to-v6/), hier die wichtigsten Punkte:

### Python 3.13 ist jetzt Pflicht

Wer noch auf <=3.12 läuft, muss vor dem Update die Python-Runtime upgraden.

### Neues `PaperlessConfig`-Objekt und Umgebungsvariablen

Der Konstruktor hat sich verändert. Neu ist außerdem die `PaperlessConfig`-Klasse:

```python
from pypaperless import Paperless, PaperlessConfig

# Via Config-Objekt
cfg = PaperlessConfig(url="http://localhost:8000", token="mytoken")
paperless = Paperless(config=cfg)

# Via Umgebungsvariablen (PYPAPERLESS_URL, PYPAPERLESS_TOKEN)
paperless = Paperless()
```

### `filter()` statt `reduce()`

`reduce()` ist weg. Das neue `filter()` funktioniert ähnlich, aber die Context-Manager-Variable ist jetzt das eigentliche Iterationsobjekt:

<div class="tb-tab__container" data-tb-component="tabs">
  <div class="tb-tab__nav" role="tablist" aria-label="Tabs"><button class="tb-tab__button tb-tab--active" type="button" role="tab"
        id="tabs-3-tab-0" aria-controls="tabs-3-panel-0"
        aria-selected="true" tabindex="0" data-tab-index="0">
        Vorher (v5)
      </button><button class="tb-tab__button " type="button" role="tab"
        id="tabs-3-tab-1" aria-controls="tabs-3-panel-1"
        aria-selected="false" tabindex="-1" data-tab-index="1">
        Nachher (v6)
      </button></div>
  <div class="tb-tab__content"><div class="tb-tab__panel tb-tab--active" role="tabpanel" tabindex="0"
        id="tabs-3-panel-0" aria-labelledby="tabs-3-tab-0" data-tab-index="0"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">filters</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;title__icontains&#34;</span><span class="p">:</span> <span class="s2">&#34;invoice&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">with</span> <span class="n">paperless</span><span class="o">.</span><span class="n">documents</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="o">**</span><span class="n">filters</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">paperless</span><span class="o">.</span><span class="n">documents</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</span></span></code></pre></div></div><div class="tb-tab__panel " role="tabpanel" tabindex="0"
        id="tabs-3-panel-1" aria-labelledby="tabs-3-tab-1" data-tab-index="1"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">filters</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;title__icontains&#34;</span><span class="p">:</span> <span class="s2">&#34;invoice&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">with</span> <span class="n">paperless</span><span class="o">.</span><span class="n">documents</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="o">**</span><span class="n">filters</span><span class="p">)</span> <span class="k">as</span> <span class="n">ctx</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">ctx</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</span></span></code></pre></div></div></div>
</div>


### Erstellen neuer Models

`draft()` heißt jetzt `create()`.

## Was ist neu?

Neben dem Umbau kommen sechs neue Services dazu:

- **`paperless.profile`** - Zugriff auf das eigene Benutzerprofil
- **`paperless.trash`** - Gelöschte Dokumente durchsuchen, wiederherstellen oder den Papierkorb leeren
- **`paperless.share_links`** - Share-Links für Dokumente erstellen und verwalten
- **`paperless.documents.history`** - Audit-Log eines Dokuments abrufen
- **`paperless.documents.bulk_edit`** - Massenoperationen auf vielen Dokumenten in einem einzigen API-Aufruf
- **`paperless.bulk_edit_objects`** - Berechtigungen oder Löschvorgänge auf Tags, Korrespondenten, Dokumenttypen und Speicherpfaden in Massen

## Fazit

v6 ist kein kleines Pflaster. Ich bin mit den Neuerungen und Änderungen sehr glücklich. Der Preis dafür ist eine Migration, die für bestehende User etwas Arbeit bedeutet.

Der [Migrationsleitfaden](https://pypaperless.docs.tbsch.de/migrating-v5-to-v6/) führt Schritt für Schritt durch alle Änderungen.

Viel Spaß mit *pypaperless* v6!

---

Das Titel-/Hintergrundbild stammt von [Chris Ried](https://unsplash.com/de/@cdr6934?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) auf [Unsplash](https://unsplash.com/de/fotos/ein-computerbildschirm-mit-einem-haufen-code-darauf-ieic5Tq8YMk?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash).

