Migration Guide: Tortoise 1.0¶
This guide covers the breaking changes and migration steps for upgrading to Tortoise ORM 1.0+ which introduces a isolated-context architecture for improved test isolation and cleaner state management.
Overview¶
Tortoise ORM 1.0 introduces a isolated-context architecture that:
Removes global state (
_default_context, metaclass)Uses
TortoiseContextas the single source of truthProvides test isolation with
tortoise_test_context()Simplifies connection management
Most application code continues to work unchanged. The main changes affect:
Direct access to the
connectionssingletonTest infrastructure (
test.TestCase,initializer, etc.)Multiple
asyncio.run()call patterns
Quick Reference¶
Old Pattern |
New Pattern |
|---|---|
|
|
|
|
|
|
|
pytest + |
|
|
What Stays the Same¶
The following APIs continue to work unchanged:
# Initialization (unchanged)
await Tortoise.init(config=...)
await Tortoise.init(db_url="...", modules={...})
await Tortoise.generate_schemas()
# Accessing apps (unchanged)
Tortoise.apps
Tortoise._inited
# Model operations (unchanged)
await User.create(name="test")
await User.filter(name="test").first()
# Framework integrations (unchanged for users)
# FastAPI, Starlette, Sanic, etc.
Connection Access Changes¶
Old Pattern (Deprecated)¶
from tortoise import connections
conn = connections.get("default")
await connections.close_all()
New Pattern¶
from tortoise import Tortoise
# Or: from tortoise.connection import get_connection, get_connections
# Get a single connection
conn = Tortoise.get_connection("default")
# Get the connection handler
handler = get_connections()
all_conns = handler.all()
# Close all connections
await Tortoise.close_connections()
Test Migration¶
The legacy test base classes (TestCase, IsolatedTestCase, etc.) and helper
functions (initializer, finalizer) have been replaced with a pytest-based
approach using tortoise_test_context().
Old Test Pattern¶
from tortoise.contrib import test
class TestUser(test.TestCase):
async def test_create(self):
user = await User.create(name="Test")
self.assertEqual(user.name, "Test")
async def test_filter(self):
await User.create(name="Test")
users = await User.filter(name="Test")
self.assertEqual(len(users), 1)
With conftest.py:
from tortoise.contrib.test import initializer, finalizer
@pytest.fixture(scope="session", autouse=True)
def initialize_tests(request):
initializer(["myapp.models"])
request.addfinalizer(finalizer)
New Test Pattern¶
import pytest
from tests.testmodels import User
@pytest.mark.asyncio
async def test_create(db):
user = await User.create(name="Test")
assert user.name == "Test"
@pytest.mark.asyncio
async def test_filter(db):
await User.create(name="Test")
users = await User.filter(name="Test")
assert len(users) == 1
With conftest.py:
import pytest_asyncio
from tortoise.contrib.test import tortoise_test_context
@pytest_asyncio.fixture
async def db():
async with tortoise_test_context(["myapp.models"]) as ctx:
yield ctx
Migration Checklist¶
For each test file:
Replace
from tortoise.contrib import testwithimport pytestRemove class wrapper (
class TestXxx(test.TestCase):)Add
@pytest.mark.asynciodecorator to each async testAdd
dbfixture parameter to each test functionReplace assertion methods: -
self.assertEqual(a, b)→assert a == b-self.assertIn(a, b)→assert a in b-self.assertRaises(Exc)→pytest.raises(Exc)-self.assertTrue(x)→assert x-self.assertFalse(x)→assert not x
Multiple asyncio.run() Calls (Uncommon Pattern)¶
Note
This section only applies if you use multiple separate asyncio.run() calls
in sequence. The typical pattern of a single asyncio.run(main()) that contains
all ORM operations continues to work unchanged.
If you use multiple separate asyncio.run() calls (sometimes seen in scripts or REPL
sessions), the ContextVar that tracks ORM state is lost between runs due to Python’s
ContextVar scoping rules. This pattern now requires explicit context management.
As a fallback _enable_global_fallback on Tortoise.init(…) can be used to set created context as global fallback.
Old Pattern (No Longer Works)¶
import asyncio
from tortoise import Tortoise
# Context is lost after asyncio.run() completes
asyncio.run(Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]}))
asyncio.run(User.create(name="test")) # FAILS: No context
New Patterns¶
Option 1: Single asyncio.run (Recommended)
import asyncio
from tortoise import Tortoise
async def main():
await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
await Tortoise.generate_schemas()
user = await User.create(name="test")
print(f"Created user: {user.id}")
await Tortoise.close_connections()
asyncio.run(main())
Option 2: Capture and Reuse Context
import asyncio
from tortoise import Tortoise
# Tortoise.init() returns the context
ctx = asyncio.run(Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]}))
# Re-enter context for subsequent runs
with ctx:
asyncio.run(Tortoise.generate_schemas())
asyncio.run(User.create(name="test"))
Option 3: Explicit Context Manager
import asyncio
from tortoise.context import TortoiseContext
with TortoiseContext() as ctx:
asyncio.run(ctx.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]}))
asyncio.run(ctx.generate_schemas())
asyncio.run(User.create(name="test"))
Using TortoiseContext Directly¶
For advanced use cases (testing, multi-tenant applications), you can use
TortoiseContext directly:
from tortoise.context import TortoiseContext
async def run_isolated():
async with TortoiseContext() as ctx:
await ctx.init(
db_url="sqlite://:memory:",
modules={"models": ["myapp.models"]}
)
await ctx.generate_schemas()
# All ORM operations use this context
user = await User.create(name="test")
# Context auto-closes on exit
Benefits of TortoiseContext:
Test isolation: Each context has independent connections and state
Multi-tenancy: Different contexts can connect to different databases
No global state: Clear ownership of ORM state
Automatic cleanup: Connections close when context exits
Framework Integration Changes¶
If you use the built-in framework integrations (FastAPI, Starlette, etc.), no changes
are required. The integrations have been updated internally to use Tortoise.close_connections()
instead of connections.close_all().
Multiple FastAPI Apps (Global Fallback)¶
When using RegisterTortoise with FastAPI, a global fallback context is enabled by default.
This allows Tortoise ORM to work correctly with asgi-lifespan (used in tests) where the
lifespan runs in a separate background task from the requests.
If you run multiple FastAPI apps in the same process (e.g., in tests), you may encounter:
ConfigurationError: Global context fallback is already enabled by another Tortoise.init() call.
Solution: Disable global fallback for secondary apps and use explicit context access:
# main_app.py - Primary app (uses global fallback)
from tortoise.contrib.fastapi import RegisterTortoise
@asynccontextmanager
async def lifespan(app: FastAPI):
async with RegisterTortoise(
app,
db_url="sqlite://:memory:",
modules={"models": ["myapp.models"]},
):
yield
app = FastAPI(lifespan=lifespan)
# secondary_app.py - Secondary app (explicit context)
from tortoise.contrib.fastapi import RegisterTortoise
@asynccontextmanager
async def lifespan(app: FastAPI):
async with RegisterTortoise(
app,
db_url="sqlite://:memory:",
modules={"models": ["myapp.models"]},
_enable_global_fallback=False, # Disable global fallback
):
yield
app_secondary = FastAPI(lifespan=lifespan)
In tests, access the secondary app’s context explicitly via app.state:
@pytest.fixture
async def client_secondary():
async with LifespanManager(app_secondary) as manager:
# Get context from app.state and enter it
ctx = app_secondary.state._tortoise_context
with ctx: # Make context current via contextvar
async with AsyncClient(app=app_secondary) as c:
yield c
The _enable_global_fallback parameter:
True(default): Sets context as global fallback for cross-task accessFalse: Context only accessible viaapp.state._tortoise_context
This is also available in Tortoise.init() (default False) and
TortoiseContext.init() (default False).
Custom Integration Migration¶
If you’ve written custom framework integrations:
# Old
from tortoise import connections
async def shutdown():
await connections.close_all()
# New
from tortoise import Tortoise
async def shutdown():
await Tortoise.close_connections()
Removed APIs¶
The following APIs have been removed:
test.TestCase,test.IsolatedTestCase,test.TruncationTestCasetest.SimpleTestCasetest.initializer(),test.finalizer()test.env_initializer()test.getDBConfig()
Deprecated APIs¶
The following APIs still work but are deprecated:
from tortoise import connections- useget_connection()/get_connections()instead
Still Available¶
The following APIs are still available and work as before:
init_memory_sqlite()decorator - for simple scriptsMEMORY_SQLITEconstant -"sqlite://:memory:"requireCapability()- for capability-based test skippingtruncate_all_models()- for test cleanup
Troubleshooting¶
“No TortoiseContext is currently active”¶
This error occurs when trying to access ORM features without an active context.
Solutions:
Ensure
Tortoise.init()was called before accessing modelsIf using multiple
asyncio.run()calls, use context manager patternIn tests, ensure the
dbfixture is being used
“Global context fallback is already enabled”¶
This error occurs when multiple Tortoise.init() or RegisterTortoise calls
try to enable global fallback simultaneously.
Solutions:
For multiple FastAPI apps, set
_enable_global_fallback=Falseon secondary appsAccess secondary app’s context explicitly via
app.state._tortoise_contextSee “Multiple FastAPI Apps (Global Fallback)” section above
“ConfigurationError: Connections not initialized”¶
This error occurs when trying to access connections before initialization.
Solution: Ensure Tortoise.init() or ctx.init() has been called and awaited.
Test isolation issues¶
If tests are interfering with each other:
Ensure using function-scoped
dbfixture (not session-scoped)Use
tortoise_test_context()which provides explicit isolationRemove any
@pytest.fixture(scope="session")that callsinitializer()
Getting Help¶
If you encounter issues during migration:
Check the GitHub Issues
Review the examples directory
Ask in the GitHub Discussions