swapper
The unofficial Django API for building swappable, overridable models in reusable apps.
Repository Health
Technical Analysis
Swapper is a small utility library that exposes Django’s undocumented swappable-models mechanism — the same feature that lets projects replace auth.User — for any model in any reusable app. Library authors define abstract base classes and swappable settings, and downstream users can drop in a customized subclass without forking the original app’s source.
Maintained by the OpenWISP project since 2014, Swapper is a stable, narrowly-scoped dependency: a handful of public functions covering setting names, swap detection, model loading, and migration dependency resolution, with no other runtime behavior and no external services to configure.
What You Get
- A compact, dependency-light wrapper around Django’s swappable-models settings API
- Documented functions covering setting-name generation, swap checks, model loading, and migration dependencies
- A compatibility matrix tested via tox across Python 3.10-3.13 and Django 4.2-5.2
- MIT-licensed source with no license keys, paid tiers, or usage restrictions
Common Use Cases
- Building a reusable Django app whose models end-users can swap for their own subclasses
- Writing migrations for apps with multiple layers of swappable models
- Standardizing on the same swap mechanism Django’s own auth.User uses, instead of duplicating it per project
- Adding an app-specific settings prefix for cleaner override names across a suite of related apps
Under The Hood
Architecture
Swapper is a single flat module (swapper/__init__.py) with no classes and almost no internal state beyond a module-level _prefixes dict used by set_app_prefix(). Every public function — swappable_setting, is_swapped, get_model_name, get_model_names, load_model, dependency, set_app_prefix, join/split — reads directly from Django’s own settings object and django.apps.apps registry rather than maintaining any parallel bookkeeping of its own, so the library adds essentially zero state on top of Django. load_model() is the one function with meaningful branching: it resolves the swap via is_swapped(), then delegates to apps.get_model(), catching LookupError and re-raising as Django’s own ImproperlyConfigured when a required model is missing. There are no internal layers to speak of — the design is intentionally a thin pass-through, which is appropriate for its scope but means the whole library’s correctness rides on Django’s settings and app-registry internals not changing shape.
Tech Stack
The only runtime dependency is Django itself (imports from django.apps, django.conf, django.core.exceptions, and django.db.migrations), targeting Django 4.2 through 5.2 and Python 3.10–3.13 per setup.py’s classifiers and tox.ini’s env matrix. Packaging is classic setup.py plus setuptools_scm for version derivation from git tags (no pyproject.toml), and the test matrix is driven by tox with separate noswap/swap environments selecting between two Django settings modules (tests.settings vs tests.swap_settings) to exercise both code paths. Linting runs via flake8 (configured in setup.cfg, max line length 110) as its own tox environment, wired into GitHub Actions CI (.github/workflows/ci.yml).
Code Quality
Tests live in tests/test_swapper.py using Python’s stdlib unittest combined with Django’s TestCase, with unittest.skipUnless/skipIf gating swap-specific assertions based on a settings.SWAP flag — a compact way to cover both swapped and unswapped behavior from one test file without duplicating test classes. Error handling is explicit and idiomatic: load_model() raises Django’s own ImproperlyConfigured with a clear message rather than returning None silently when required=True. Naming is consistent and mirrors Django’s own vocabulary (swappable, app_label, is_swapped), but there are no type hints anywhere in the module and no static type checker configured — only flake8 for style, run as a dedicated CI job across every supported Python version.
API Design
The public surface is deliberately small — under ten functions, each doing one thing, documented in a single README table with argument descriptions and return semantics. Naming directly echoes Django’s own internal swap vocabulary (is_swapped, swappable_setting, get_model_name), so anyone who has read Django’s auth.User swap docs already knows Swapper’s API. Getting started requires one import and a couple of calls in models.py and a migration file; there’s no configuration object, no client to instantiate, and no boilerplate beyond calling the relevant function where Django itself would otherwise require hand-written swap logic.