NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #4180 most downloaded on PyPI
Tenant support for Django using PostgreSQL schemas.
Last release 2 months ago
05 Aug 2026
Ships fairly regularly
a new release about every 3 months
Most releases are documented
notes for 45 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
11 years old
61 releases · first in 2015
Django 4.2 support dropped (#1248). Django 4.2 LTS reached end of extended support on 7 April 2026 and is now listed upstream under "Unsupported previ
Django 4.2 support dropped (#1248). Django 4.2 LTS reached end of extended support on 7 April 2026 and is now listed upstream under "Unsupported previous releases" — it receives no further security or bug fixes. The minimum is now Django 5.2 LTS, which is supported until April 2028.
django>=5.2,<6.2
The pin is enforced, not merely declared — resolving against 4.2 fails rather than quietly working. If you are still on 4.2, stay on 3.13.0 until you can move to 5.2.
Four version shims went with it: the ImportError fallback around is_psycopg3 (the psycopg_any module has existed since 4.2, so that branch was already unreachable), the < (5, 2) fork over _pre_setup, the all / find_all keyword choice in the finder tests, and the DEFAULT_FILE_STORAGE fallback documented in docs/files.rst for "django < 4.2".
Django 6.1 support (#1247, closes #1246). django-tenants did not merely fail tests on 6.1 — it failed to import at all, so manage.py could not start:
ImportError: cannot import name 'DatabaseIntrospection' from partially initialized
module 'django.db.backends.postgresql.introspection'
Django 6.1 added from django.db.backends.postgresql.base import psycopg_version to its PostgreSQL introspection module. base already imports introspection, so the two now form a cycle. Django itself is unaffected because base is always imported first in normal use; the cycle only breaks for a third party importing introspection first, which we did. Fixed by importing the backend's base module first — pure ordering, no behaviour change on any version.
The staticfiles finder tests also used find(path, all=True). That keyword was renamed to find_all in 5.2 and the old spelling removed in 6.1 — note there is no find_all() method, only the renamed keyword. TenantFileSystemFinder does not override find(), so this was test-only.
CI now covers ==6.1.*, installing Django with --pre so a series without a final release yet resolves to its release candidate. That turned out to matter: pip 26.2.1 resolved Django==6.1.* to 6.1rc1 unaided, while pip 25.0.1 reported No matching distribution found — and setup-python bundles whatever pip ships with each interpreter.
search_path handling is now driver-agnostic, and psycopg2 is tested (#1249, fixes #1244). _cursor() special-cased psycopg3, never reusing the caller's cursor for SET search_path:
if name or is_psycopg3:
cursor_for_search_path = self.connection.cursor() # raw driver cursor
else:
cursor_for_search_path = cursor # Django CursorWrapper
Because self.connection.cursor() returns a raw driver cursor, the statement bypassed Django's cursor wrapper — so on psycopg3 it was invisible to assertNumQueries, while on psycopg2 it was counted. Two tests therefore failed on psycopg2 and had never passed there:
AssertionError: 6 != 3 : 6 queries executed, 3 expected
The special case existed to avoid a recursion. A _setting_search_path flag now makes that re-entry a no-op instead, so the caller's cursor can be reused on both drivers and the SET logic — moved into _handle_search_path() — branches only on whether a cursor was supplied, never on the driver.
This changes no database traffic. With log_statement='all', 3.13.0 and this release both send exactly 646 SET search_path statements for the same test; only their visibility to the wrapper differs. The updated assertion counts are therefore more accurate, not looser — they count statements that were always executed.
CI gains a psycopg-version axis, so the psycopg2 fallback is now exercised on every run rather than being supported in code and untested in practice.
Cached search_path is cleared on rollback() (#1249). A session-level SET is transactional in PostgreSQL — aborting the transaction that issued it reverts the search_path. With TENANT_LIMIT_SET_CALLS enabled, search_path_set_schemas could outlive a value the database had already discarded.
Every search path component is validated (#1249). _get_cursor_search_paths() checked only self.schema_name and then extended the list with PG_EXTRA_SEARCH_PATHS unvalidated, despite all parts being interpolated into the SET statement. All of them now go through _check_schema_name.
Tenant-isolation tests for QuerySet.iterator() (#1245). The named-cursor branch of _cursor() had no coverage, and it is the one place in the backend where a mistake reads as working code: a named cursor resolves its table names when Postgres runs DECLARE, so a search_path that had not been set yet would return another tenant's rows rather than raising. The new tests assert against pg_cursors — Postgres' own view of open cursors — so they cannot pass by silently falling back to a client-side fetch. Verified by mutation: breaking the SET search_path makes them fail with tenant 2's rows where tenant 1's were expected.
Worth recording, since it comes up regularly: DISABLE_SERVER_SIDE_CURSORS is not a django-tenants requirement. It appears nowhere in the library or its docs, and .iterator() is tenant-safe without it. Both configurations are now pinned by tests, so choosing between them stays a connection-pooler question rather than a tenancy one.
The "How it works" links in the README and docs index now point somewhere (#1243).
.iterator() and transaction pooling interact in a way that is easy to get backwards. Server-side cursors cannot survive transaction pooling, so a pooler in that mode requires DISABLE_SERVER_SIDE_CURSORS = True — at which point .iterator() fetches the entire result set client-side and no longer bounds memory. A pooler is therefore an obstacle to .iterator(), never a prerequisite for it.
Separately, django-tenants still issues session-level SET search_path, which leaks between tenants under transaction pooling. Session pooling is safe; transaction pooling is not yet supported. #1112 tracks it.
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.13.0...v3.14.0
One column per quarter.
`STORAGES` in the test project and examples (#1240, fixes #1095, #1173). DEFAULT_FILE_STORAGE and STATICFILES_STORAGE were deprecated in Django 4.2 an…
Cloning a schema owned by a role whose name needs quoting (#1239, fixes #1189). Role names were interpolated into generated DDL raw, so a role containing a hyphen — or starting with a digit — made the statement a syntax error and the clone failed outright:
CREATE SCHEMA new_tenant AUTHORIZATION my-role
^ syntax error at or near "-"
Three places were affected: CREATE SCHEMA ... AUTHORIZATION, ALTER SEQUENCE ... OWNER TO, and the ALTER TABLE ... OWNER TO in pg_get_tabledef. Table and type owners were already quoted at their SELECT and are unchanged. This also resolves the long-standing #561, which was the same bug seen through a role name beginning with a digit.
STORAGES in the test project and examples (#1240, fixes #1095, #1173). DEFAULT_FILE_STORAGE and STATICFILES_STORAGE were deprecated in Django 4.2 and removed in 5.1, where they are silently ignored — but the test project and all three example tutorials still set them. Anyone who copied an example was quietly getting single-tenant file storage:
STORAGES['default'] |
STORAGES['staticfiles'] |
|
|---|---|---|
| before | FileSystemStorage |
StaticFilesStorage |
| after | TenantFileSystemStorage |
TenantStaticFilesStorage |
Nothing errored, which is why it went unnoticed. docs/files.rst now leads with STORAGES, keeping DEFAULT_FILE_STORAGE documented for Django < 4.2, which is still supported.
get_current_tenant() (#1241, closes #1217). get_tenant() takes a request, so there was no way to ask which tenant am I in from a helper function, a Celery task, or a signal handler:
from django_tenants.utils import get_current_tenant
def send_welcome_email(user):
tenant = get_current_tenant() # no request needed
tenant_context() sets the real instance, so that is what comes back. schema_context() only ever knows a schema name, so a FakeTenant is returned there — read its schema_name, or use tenant_context() when you need the model.
migrate_schemas no longer accepts --list / -l (#794). Nothing ever read the flag — Django moved that functionality to showmigrations long ago — so it silently did nothing. Passing it now raises unrecognized arguments rather than being accepted and ignored. Use showmigrations, or tenant_command showmigrations --schema=<name> for a single tenant.
is_public_schema's unused app argument is now optional, so both {% is_public_schema %} and {% is_public_schema app %} work (#339).transaction.atomic() — fixed in 3.12.0 by #1150 but shipped without a test, so nothing prevented transaction.commit() returning (#1155, #694).The issue tracker has had a thorough pass: 254 open issues down to 88. Everything closed carries a reason, and anything closed as stale can be reopened — if one of them is still biting you on 3.13.0, please do, ideally with the versions and steps to reproduce.
The missing v3.8.0 (#1156) and v3.10.1 (#1222) tags have also been created, pointing at the commits those releases were built from.
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.12.0...v3.13.0
The wheel no longer installs `docs`, `examples` and `dts_test_project` as top-level packages (#1224, thanks @Ramblurr). Every release up to 3.10.2 shi
The wheel no longer installs docs, examples and dts_test_project as top-level packages (#1224, thanks @Ramblurr). Every release up to 3.10.2 shipped 159 stray files into site-packages as importable top-level names — including dts_test_project, which could shadow a project's own module. If you were importing any of them, accidentally or otherwise, that will now fail. They remain in the sdist.
Cloning a tenant works again when a model has ever been renamed (#1236). clone_schema failed with relation "<dest>.<old table name>_id_seq" does not exist on any schema containing a renamed table that holds data. Postgres derives a sequence's name from its table when the column is created and never revisits it, so ALTER TABLE ... RENAME — any Django RenameModel — leaves the sequence under the table's old name, while the destination's sequences are created fresh by CREATE TABLE ... (LIKE ... INCLUDING ALL) and named after the current table. The two diverge for every renamed table. Identity sequences are now found through pg_depend and the destination's own sequence resolved with pg_get_serial_sequence.
clone_schema can be called inside transaction.atomic() (#1150, thanks @Winnie-Fred). It called transaction.commit(), which raises TransactionManagementError inside an atomic block — so cloning from the Django admin or a TestCase was impossible. Fixes #1155 and #694.
Cloning no longer fails silently when TENANT_BASE_SCHEMA does not exist (#1133, thanks @hassaanalansary) — it falls back to creating the schema, which matters on a first run against an empty database.
A subprocess migration executor (#1228, thanks @tjwalch). migrate_schemas normally runs every tenant in one long-lived process, and per-tenant state is never released, so resident memory climbs until the OOM killer reaps the process mid-migration. --executor=subprocess spawns a fresh manage.py migrate_schemas --schema <name> per tenant so memory returns to baseline between them, with --parallel N / TENANT_SUBPROCESS_PARALLEL for concurrency. Opt-in; existing behaviour is unchanged. Fixes #1211.
A mypy plugin (#1231, thanks @bvedad). plugins = ["django_tenants.mypy_plugin"] declares request.tenant on HttpRequest, so strict mypy with django-stubs >= 6.0 stops reporting attr-defined on every access. Subclasses, including DRF's Request, inherit it.
Releases now publish to PyPI automatically via a release.yml workflow using trusted publishing (OIDC) — no API token in the repository. The build refuses to publish when the git tag disagrees with the version in pyproject.toml.
literalinclude for the settings examples so they can't drift from tested code (#1146, thanks @foarsitter); the tenant-schema context manager is now documented (#1215, thanks @fairwiz); grammar in the Admin Support section (#1234, thanks @NaftaliHolland).
This follows 3.10.2, the previous PyPI release. There is an earlier v3.11.2 tag in this repository, but it points at the "Bump version to 3.10.2" commit, contains 3.10.2, and was never published to PyPI. Numbering this release in the 3.11.x range would have sorted below that tag, so it goes out as 3.12.0.
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.10.0...v3.12.0
Nothing published for this version
Nothing published for this version
Add django>4.2 style STORAGES definition by @voidus in https://github.com/django-tenants/django-tenants/pull/1187
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.9.0...v3.10.0
Bump sphinx from 8.1.3 to 8.2.3 by @dependabot[bot] in https://github.com/django-tenants/django-tenants/pull/1130
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.7.8...v3.9.0
Nothing published for this version
Return tenant delete result by @mikicz in https://github.com/django-tenants/django-tenants/pull/1006
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.6.1...v3.7.0
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.6.0...v3.6.1
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.6.0...v3.6.1
Add --no-input flag to delete_tenant command by @thraxil in https://github.com/django-tenants/django-tenants/pull/945
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.5.0...v3.6.0
Bump django from 4.0.9 to 4.1.7 by @dependabot in https://github.com/django-tenants/django-tenants/pull/893
Full Changelog: https://github.com/django-tenants/django-tenants/compare/3.4.8...v3.5.0
Fix DeprecationWarning: invalid escape sequence '\.' by @cclauss in https://github.com/django-tenants/django-tenants/pull/862
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.4.7...3.4.8
Revert "Only clear contenttypes cache when necessary" by @atodorov in https://github.com/django-tenants/django-tenants/pull/859
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.4.6...v3.4.7
Added a simple helper function to get the tenant object
Full Changelog: https://github.com/django-tenants/django-tenants/compare/v3.4.5...v3.4.6
Remove deprecated url imports from example projects by @algebra7 in https://github.com/django-tenants/django-tenants/pull/806
Full Changelog: https://github.com/django-tenants/django-tenants/compare/V3.4.4...v3.4.5
made it work with Django 4.1 #780
Added support for providing AppConfig in app lists #718
Fixed a problem installing on Django 4.0.x
Reverted #676 as it caused some issue
Fixed a problem with collect_static for django 3.2 and above #698
Fixed a bug a problem cloning a tenant #668
Added create_missing_schemas management command
Added create_missing_schemas management command
Added new setting SHOW_PUBLIC_IF_NO_TENANT_FOUND
Nothing published for this version
Fixed #466 a problem with the setup.py and Django 3.1
- #434 Allowed for Django 3.1 - #432 Added a beta version of subfolder tenants. - #458 Add a signal that gets sent after migrations finish for a schem
Allowed a schema to be renamed. #388
A fix to the clone_tenant that stopped it working on certain systems
A fix to the clone_tenant that stopped it working on certain systems
Fixed #322 You can now clone a tenants with the clone_tenant command Fixed some issues with docker-compose and the example project Fixed #325 Refactor
Fixed #322 You can now clone a tenants with the clone_tenant command Fixed some issues with docker-compose and the example project Fixed #325 Refactor some classes/methods
You can now clone a tenants with the clone_tenant command #322
Fixed some issues with docker-compose and the example project
Refactor some classes/methods #325
Make "test login" add tenant to request #340
Make "test login" add tenant to request #340
Fixes
Use field.attname instead of field.name in create_tenant.py
Removed psycopg2 as a requirement #317
Fixes
collectstatic_schemas, were not being found using TenantStaticFilesStorage. Fixes #265 <https://github.com/tomturner/django-tenants/issues/265>_.Fixed an issue in setup.py to allow different colour of tenant apps in the admin area. Should now work with PyPi.
Fixes
Fixes
Fixed an issue in setup.py to allow different color of tenant apps in the admin area. Should now work with PyPi.
Fixed an issue with the different colour of tenant apps in the admin area. [#261]
Fixes
Fixes
Fixed an issue in setup.py to allow different color of tenant apps in the admin area. Take 2. [#262]
Fixed an issue with the different color of tenant apps in the admin area. [#261 _]
Fixes
Fixed an issue with the different color of tenant apps in the admin area. [#261]
TenantFileSystemStorage now works [#249]
Fixes
Enhancements
Added Django 2.1 Support More notes to follow
Added Django 2.1 Support
More notes to follow
Enhancements
Added TENANT_CREATION_FAKES_MIGRATIONS configuration parameter that can be used to copy schemas from an existing "template" schema instead of running migrations.
schema_context now operates as a decorator too. Fixes: #199.
Update template loaders and static file storage for Django > 2.0. Fixes: #197.
Added pre_drop to TenantMixin that can be used to backup the tenant schema before dropping.
Allow using non-default databases for schemas using the new TENANT_DB_ALIAS config parameter.
Add TENANT_BASE_SCHEMA configuration parameter for creating tenant schema from a pre-specified "default" base tenant schema.
Add support for tenant models that are not serializable.
Various updates to documentation.
Update tests for Django 2 and Python 3.
Fixes
Fix setup.py to reference new psycopg2-binary dependency. Fixes #174.
Add support for creating tenants that share field names with domains. Fixes: #167.
Use get_tenant instead of get_domain in DefaultTenantMiddleware to lookup tenant. Fixes: #154.
Fix TENANT_LIMIT_SET_CALLS implementation to not rely on the cursor pointer changes. See: #157.
Django version 2 support. This version doesn't work with any of the Django 1 version
Django version 2 support. This version doesn't work with any of the Django 1 version
TEMPLATE_CONTEXT_PROCESSORS deprecated …
Warning Middleware is now in a directory
Updates for Django 1.11
#128 - Does django-tenants work with Django 1.11? #125 - Adding a safety check inside tenant model save and delete for compatibility
Warning Middleware is now in a directory
Nothing published for this version
Nothing published for this version
Nothing published for this version
Fixed #107 a Django 1.10 error Fixed #109 missing files on pypi
Fixed #107 a Django 1.10 error Fixed #109 missing files on pypi
Made it so the tests run with python 3
More Django 1.10 stuff #95 fixed tenant_command for django 1.10 #100 Added fast test cases #101 Added logging #102 Added reverse
More Django 1.10 stuff
#95 fixed tenant_command for django 1.10
#100 Added fast test cases
#101 Added logging
#102 Added reverse
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Fixed a problem in Django 1.9 todo with urlresolvers Some change to allow the test to work in Python 3 (Untested)
Fixed a problem in Django 1.9 todo with urlresolvers Some change to allow the test to work in Python 3 (Untested)
Added Clone Tenant management command
Added Clone Tenant management command
More fixes to parallel migrations
More fixes to parallel migrations
A fix to the setup.py for migration_executors
A fix to the setup.py for migration_executors
Added migration executor layer #39
Added migration executor layer #39
Fixed a problem with migrations #20
Added PostGis support #22
Fixed a tenant_command #3
Fixed a problem with migrations #20
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →