pytest-sqla
Every test gets a SQLAlchemy session whose work is rolled back, sync or async,
even when your code calls commit().
def test_signup(sqla_session):
signup(sqla_session, "ada@example.com") # commits, as production code does
assert sqla_session.scalar(select(func.count(User.id))) == 1
# ...and the next test starts from an empty table again.
pytest-sqla is a pytest plugin for SQLAlchemy 2.0 and 2.1. It gives each test
one connection inside an outer transaction, binds sessions to it with
join_transaction_mode="create_savepoint", and rolls everything back at
teardown. That is SQLAlchemy's own documented recipe, "Joining a Session into
an External Transaction"; the plugin is the dozen details around it
that a 30-line conftest gets wrong.
Why this exists
The plugin most projects find first, pytest-async-sqlalchemy, last shipped in
October 2021 and fails before its first test on pytest-asyncio 1.x, with
fixture 'event_loop' not found. The others are sync-only, or leave the
SQLite workaround to you, or monkeypatch SQLAlchemy. pytest-sqla gets six hard
parts right on its own, each one measured:
- Commit and rollback stay inside the test. The default join mode turns
into
rollback_onlyon a connection that is already in a transaction. An application'srollback()after an IntegrityError then rolls back the rows the test seeded, and its nextcommit()survives the test.create_savepointkeeps both inside. - SQLite actually rolls back. In sqlite3's legacy transaction control, Python's default through 3.15, a row committed through a savepoint session survives the outer rollback. pytest-sqla applies SQLAlchemy's documented fix itself.
- In-memory SQLite is one database.
sqlite://is private to a single connection; pytest-sqla shares it between the sync engine, the async engine and the schema through SQLite's memdb VFS. - Async objects live on the test's own event loop, under all five pytest-asyncio loop scopes, in strict and auto mode, and under anyio. A connection used from another loop raises a clear error before the driver sees it; asyncpg, for one, is left unusable by such a call.
- The schema is created once, even under pytest-xdist. A naive
create_allper worker errors on every run against PostgreSQL and MySQL; pytest-sqla serialises it under a database lock and never drops a table it did not create. - Breaches fail loudly, in the test that caused them. A commit of the connection itself, or MySQL committing DDL on its own, fails that test's teardown, so the leak is reported where it happened instead of surfacing as a mystery in some later test.
Install
pip install pytest-sqla # sync fixtures
pip install "pytest-sqla[asyncio]" # adds pytest-asyncio and greenlet
The two runtime dependencies are sqlalchemy>=2.0 and pytest>=8.2. The
asyncio extra adds pytest-asyncio>=1.2 and sqlalchemy[asyncio]: SQLAlchemy
2.1 no longer installs greenlet, which its asyncio extension cannot import
without. anyio users already have anyio through Starlette, so they need
pip install pytest-sqla "sqlalchemy[asyncio]".
Quick start
# pyproject.toml
[tool.pytest.ini_options]
sqla_url = "sqlite://"
sqla_metadata = "app.models:Base"
from sqlalchemy import func, select
from app.models import User
def test_commits_are_rolled_back(sqla_session):
sqla_session.add(User(email="ada@example.com"))
sqla_session.commit()
assert sqla_session.scalar(select(func.count()).select_from(User)) == 1
def test_and_the_next_test_starts_clean(sqla_session):
assert sqla_session.scalar(select(func.count()).select_from(User)) == 0
The tables in app.models:Base are created before the first database test and
dropped after the last one. For the async fixtures, install the asyncio extra
and give them an async driver:
[tool.pytest.ini_options]
sqla_url = "sqlite://"
sqla_async_url = "sqlite+aiosqlite://" # the same in-memory database; needs aiosqlite
sqla_metadata = "app.models:Base"
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
import pytest
@pytest.mark.asyncio
async def test_async_commits_are_rolled_back_too(sqla_async_session):
sqla_async_session.add(User(email="ada@example.com"))
await sqla_async_session.commit()
In CI, point the same tests at a real server through the environment, which takes precedence over the ini file:
env:
PYTEST_SQLA_URL: postgresql+psycopg://app:app@localhost:5432/app_test
PYTEST_SQLA_ASYNC_URL: postgresql+psycopg://app:app@localhost:5432/app_test
postgresql+psycopg serves both the sync and the async fixtures. Set the async
variable whenever the ini file sets sqla_async_url: the two URLs must name the
same database, and pytest-sqla refuses to run with one on PostgreSQL and the
other still on SQLite. With MySQL, use mysql+pymysql and mysql+aiomysql.
FastAPI
Override the application's session dependency with sqla_async_sessionmaker.
Every request gets its own AsyncSession, all of them inside the test's
transaction, so the application commits and rolls back as it does in
production, and the next test sees none of it.
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from app.main import app, get_session
@pytest.fixture(scope="session")
def sqla_session_options():
return {"expire_on_commit": False} # as the app's own sessionmaker
@pytest_asyncio.fixture
async def client(sqla_async_sessionmaker):
async def override():
async with sqla_async_sessionmaker() as session:
yield session
app.dependency_overrides[get_session] = override
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
yield client
app.dependency_overrides.clear()
@pytest.mark.asyncio
async def test_a_duplicate_email_is_a_conflict(client):
assert (await client.post("/users", params={"email": "ada@example.com"})).status_code == 201
assert (await client.post("/users", params={"email": "ada@example.com"})).status_code == 409
assert (await client.get("/users/count")).json() == {"count": 1}
@pytest.mark.asyncio
async def test_the_previous_tests_commit_was_rolled_back(client):
assert (await client.get("/users/count")).json() == {"count": 0}
Use httpx.AsyncClient with ASGITransport, not Starlette's TestClient.
TestClient runs the application in a thread with an event loop of its own, and
the test's connection belongs to the test's loop. Match sqla_session_options
to your own sessionmaker: with the default expire_on_commit=True, reading
user.id after commit() reloads it with blocking IO, which SQLAlchemy's
asyncio extension refuses with MissingGreenlet.
This example runs in CI on SQLite, PostgreSQL and MySQL, under pytest-asyncio
and under anyio: see
examples/.
Fixtures
| Fixture | Scope | Family | What it gives you |
|---|---|---|---|
sqla_url |
session | config | The configured database URL, or None. Override it to compute one. |
sqla_async_url |
session | config | The async fixtures' URL; sqla_url unless set separately. |
sqla_metadata |
session | config | The metadata to create, from the sqla_metadata ini key or an override. |
sqla_engine_options |
session | config | Keyword arguments for every engine. Default {}. |
sqla_session_options |
session | config | Keyword arguments for the sessionmakers. Default {}. |
sqla_schema |
session | config | Creates the tables once, drops them at the end. Override it for Alembic. |
sqla_connection |
function | sync | A Connection inside the test transaction. |
sqla_sessionmaker |
function | sync | A sessionmaker bound to it, for dependency overrides. |
sqla_session |
function | sync | A Session from it, closed at teardown. |
sqla_async_connection |
function | async | An AsyncConnection inside the test transaction, on the test's loop. |
sqla_async_sessionmaker |
function | async | An async_sessionmaker bound to it. |
sqla_async_session |
function | async | An AsyncSession from it, closed at teardown. |
Within one test, every session from a sessionmaker shares the test's
connection: commit() releases a savepoint, rollback() rolls back to one,
and close() leaves the connection open. A test uses one family: the sync and
async fixtures hold separate connections and transactions, so they could not
see each other's writes, and asking for both is an error.
Configuration
| Setting | Command line | Environment | ini key | Default |
|---|---|---|---|---|
| Database URL | --sqla-url |
PYTEST_SQLA_URL |
sqla_url |
none |
| Async URL | --sqla-async-url |
PYTEST_SQLA_ASYNC_URL |
sqla_async_url |
the database URL |
| Metadata | sqla_metadata = "app.models:Base" |
none | ||
| Schema mode | sqla_schema = create or none |
create |
||
| Lock, DDL and teardown timeout, seconds | sqla_lock_timeout |
30 |
||
| Replace a crashed run's schema | --sqla-recreate-schema |
off |
The command line beats the environment, which beats the ini file: the ini value is the repository's committed default, the environment is per machine or CI job, and the command line is per run. Overriding a fixture in a conftest replaces the computed value, as with any pytest fixture.
- No database, no silent fallback. Without a URL, every test that asks for a database fixture fails at setup with a message naming the three ways to set one; tests that do not are unaffected. pytest-sqla never falls back to SQLite on its own, because a CI job whose URL never arrived would then test SQLite and look green. A variable that is set but empty is an error for the same reason.
- Only its own variables. pytest-sqla never reads your application's
DATABASE_URL, so it cannot pick up a database that is not for tests. - Checked before the run. The URLs, the schema mode, the timeout and the
shape of
sqla_metadataare checked at startup; a bad one stops the run with a usage error that never repeats the URL's password. - Inert unless asked. A run whose tests never request an
sqla_*fixture imports no application code, opens no connection, and never importssqlalchemy.ext.asyncio. - The report header says which database the run is configured for:
sqla: url=postgresql+psycopg://app:***@localhost/app_test (from PYTEST_SQLA_URL), schema=create. It shows the configuration, read before any test runs, so an override of thesqla_urlfixture, such as a container's URL, is not in it; the URL a test connects to issqla_connection.engine.url.
sqla_engine_options reaches every engine pytest-sqla creates: echo,
connect_args, json_serializer, execution_options and so on. It refuses
what would make a rollback impossible: isolation_level="AUTOCOMMIT" in any
spelling SQLAlchemy accepts, autocommit=True in connect_args for any driver,
and an autocommit query parameter in a server URL, which pymysql reads as
true even when it says false. It refuses
isolation_level="READ UNCOMMITTED", which lets a test read another xdist
worker's uncommitted rows, and, for async engines, poolclass and pool,
which pytest-sqla picks per event loop. The same options reach the sync and the
async engines, so keep driver-specific connect_args to a URL whose driver
serves both.
Each test gets a DBAPI connection of its own: the sync engine is NullPool,
because ROLLBACK undoes rows but not a connection's session state, and a
pooled connection hands that state to the next test (measured: MySQL's SET FOREIGN_KEY_CHECKS = 0, temporary tables and sql_mode, PostgreSQL's
session advisory locks and PREPAREd statements, SQLite's PRAGMA query_only). Give sqla_engine_options a poolclass to pool the sync
engine anyway, and accept that state then carries over; the async engines of
wider loop scopes are pooled the same way (see Pooling). pool_size,
max_overflow, pool_timeout and pool_use_lifo reach only pooled engines;
the others leave them out.
sqla_session_options reaches the sessionmakers, so the test sessions can
behave like the application's: expire_on_commit, autoflush, info, or
class_ for sqlmodel's Session and AsyncSession. It refuses bind,
binds and join_transaction_mode.
The metadata can be a MetaData, any class whose .metadata is one (a
DeclarativeBase, declarative_base(), SQLModel), or a list of these:
@pytest.fixture(scope="session")
def sqla_metadata():
return [billing.Base, accounts.Base]
Schema
sqla_schema = create, the default, creates the metadata's tables before
the first database test and drops them after the last one, once per run. It
records what it created in a small marker table, pytest_sqla, and uses it to
keep three promises:
- It never drops a table it did not create. If a metadata table already
exists without the marker, every database test fails with
SchemaConflictError naming the tables, and nothing is touched. Point
sqla_urlat a database used only for tests. - It notices a crashed run, or one still going. A run killed before its
teardown leaves the marker behind, and so does a run that is still running.
The next run refuses that schema, says when and where the other run
started, and suggests
--sqla-recreate-schema, which drops and recreates it. - It never hangs on PostgreSQL, MySQL, MariaDB and SQLite. Creation and
drop wait at most
sqla_lock_timeoutseconds for a lock. A test that leaked a transaction on a table makes the drop fail with a message saying so, and recreating the schema of a run that is still going fails the same way, instead of blocking the run forever.
On MySQL, where DDL commits on its own, a creation that fails half way drops the tables it had already created. A SQLite database pytest-sqla made up, an in-memory one or a per-worker file, is discarded at the end instead of having its tables dropped.
sqla_schema = none runs no DDL at all, for a database migrated before the
run, for example by alembic upgrade head in a CI step.
Overriding the sqla_schema fixture replaces all of it. Set
sqla_schema = none as well, so the URL is used exactly as configured, and use
a file or server database: an in-memory SQLite database exists only inside
pytest-sqla's own connections.
import alembic.command
import alembic.config
import pytest
from sqlalchemy import make_url
@pytest.fixture(scope="session")
def sqla_schema(sqla_url):
config = alembic.config.Config("alembic.ini")
# sqla_url may be a str or a URL. Alembic's config reads % as interpolation,
# and a rendered URL percent-encodes its password, so every % is doubled.
url = make_url(sqla_url).render_as_string(hide_password=False)
config.set_main_option("sqlalchemy.url", url.replace("%", "%%"))
alembic.command.upgrade(config, "head")
yield
alembic.command.downgrade(config, "base")
The suite runs this block as written, against SQLite and against PostgreSQL with a password that needs escaping. Under pytest-xdist such an override runs once per worker, so it has to serialise itself.
Async details
- pytest-asyncio, strict or auto mode. In strict mode, mark async tests
with
@pytest.mark.asyncio; pytest-sqla's async fixtures need no marking, because they are plain sync fixtures that hand you objects opened on the right loop. A plaindeftest cannot use them, even through an async fixture: there is no test loop to open the connection on. - Every loop scope.
@pytest.mark.asyncio(loop_scope="module"), theasyncio_default_test_loop_scopeini key, class, package and session loops all work: pytest-sqla reads the test's loop scope and opens its connection on that loop. - anyio.
@pytest.mark.anyiotests work with and without pytest-asyncio installed, with pytest-asyncio in strict mode. In auto mode pytest-asyncio would run an anyio test's async fixtures on its own loop, so pytest-sqla refuses that combination with an explanation. SQLAlchemy's asyncio extension runs only on asyncio, so the trio backend is refused too. A pytest-asyncio test that hasanyio_backendamong its fixtures, typically through a helper fixture written for anyio tests, is still a pytest-asyncio test. Whether anyio then runs its async fixtures, pytest-sqla's included, on a loop of its own depends on which of the two plugins registered first; when it does, the loop guard says so. Keepanyio_backendout of such tests. - The loop guard. Using a test's connection from another event loop raises
LoopMismatchErrorbefore the driver sees the statement, a COMMIT or a ROLLBACK, and the connection stays usable. Measured without the guard: asyncpg, aiomysql and asyncmy fail with "attached to a different loop", and asyncpg's connection is unusable afterwards; psycopg happens to survive, so the guard treats every driver alike. The two usual causes are an async fixture on a different loop scope than the test (give it the test's loop scope), and application code that runs in its own thread with its own loop, such as Starlette's TestClient (usehttpx.AsyncClientwithASGITransport). - Tasks the test leaves running. A connection serves one operation at a
time, and a task the test started and never awaited races the teardown on
it. Measured: asyncmy hung the run forever, and the other drivers failed the
teardown with their own errors. pytest-sqla notices a task that used the
connection and is still running, closes the connection instead of rolling
it back next to that task (the database then rolls the test back), and fails
the test with IsolationError naming it. The teardown as a whole is bounded
by
sqla_lock_timeoutthe same way. Await or cancel such tasks before the test ends. - Pooling. For a per-test loop, the pytest-asyncio default, and for anyio,
the async engine is NullPool, which SQLAlchemy's asyncio docs recommend when
one engine serves several event loops. For a class, module, package or
session loop, the engine has a real pool that lives exactly as long as that
loop, and the tests on that loop share its connections: session state one of
them sets, such as
SET FOREIGN_KEY_CHECKS = 0or a session advisory lock, reaches the next. A connection whose transaction broke (an IsolationError) is discarded rather than handed on. The sync engine is NullPool too, unless you give it apoolclass.
What NullPool costs, measured with
scripts/benchmark.py
on localhost Docker (milliseconds per test, best of 5 runs of 200 isolation
cycles; re-measure on your own machine before relying on them):
| Driver | Pooled | NullPool |
|---|---|---|
| asyncpg | 1.56 | 6.95 |
| psycopg async | 1.57 | 5.15 |
| aiomysql | 1.97 | 3.12 |
| asyncmy | 1.98 | 3.18 |
| psycopg (sync) | 1.33 | 5.43 |
| pymysql (sync) | 1.24 | 6.54 |
If those milliseconds matter more than session state, give the async database tests a wider loop scope, together with their fixtures':
asyncio_default_test_loop_scope = module
asyncio_default_fixture_loop_scope = module
An async fixture left on the per-test loop while its tests run on the module
loop uses their connection from another loop, and the loop guard stops it.
For sync tests, sqla_engine_options with a poolclass is the same trade.
pytest-asyncio 1.2 is the floor: on 1.0 and 1.1, a test with a session loop that ran after tests with a module loop was measured running on a different loop than its own scope's fixtures.
SQLite notes
- The savepoint fix. pytest-sqla installs SQLAlchemy's documented connect
and begin listeners on every SQLite engine it creates, sync and async. With
connect_args={"autocommit": False}(Python 3.12+, PEP 249 mode) sqlite3 handles transactions correctly by itself, and pytest-sqla adds only the write lock below. When a future Python makes that the default, the fix switches itself off. - In-memory databases (
sqlite://,sqlite:///:memory:,sqlite+aiosqlite://) become one named database on SQLite's memdb VFS for the whole session, held open by a keeper connection, and shared between the sync and async engines. That needs SQLite 3.36 (2021) or newer. Shared-cache mode is not used: SQLite calls it obsolete, and it fails at once where memdb waits for a lock like a file does. A shared-memory URL of your own (mode=memory,vfs=memdb) is used as given, pluscheck_same_thread=Falseon pysqlite, so a test can use it from a thread. - Files. A test's transaction on a SQLite file begins
IMMEDIATE, in PEP 249 mode too, so another process waits for it instead of failing with "database is locked". Under pytest-xdist in create mode, each worker gets a file of its own next to the configured one,test-gw0.db, which pytest-sqla deletes afterwards; it never deletes a file you named. Afile:URI URL (uri=true) is not split per worker. - One file, several workers. In none mode the workers share the file, and
since every test holds the write lock until its teardown, read-only tests
included, they run one at a time: no parallelism. A worker waits for the lock
at most sqlite3's busy timeout, 5 seconds by default, so set
connect_args={"timeout": ...}above (workers - 1) times your longest test, or give each worker its own file withsqla_schema = create. - A raw
COMMITor sqlite3'sexecutescript()inside a test ends the transaction, and the test fails at teardown with IsolationError. - An unread result in a failing test. On Python 3.11 and later, a query
result left unread keeps a SQLite read lock until it is garbage-collected,
and pytest keeps the last failure's traceback. On a file you named, that can
block the drop at the end of the run; pytest-sqla then says so. A query that
a test cancels, for example with
asyncio.wait_for, kept the write lock the same way on aiosqlite; pytest-sqla collects it at that test's teardown.
MySQL notes
MySQL commits implicitly on DDL (CREATE, ALTER, DROP, TRUNCATE), on
LOCK TABLES and on SET autocommit = 1, which ends the test's transaction:
the rows the test wrote before that point may now be permanent. pytest-sqla
notices, because its guard savepoint is gone, fails that test with
IsolationError, and discards the connection. Run such statements outside the
sqla fixtures.
A deadlock is not a breach. InnoDB rolls the victim's whole transaction back,
savepoints included, and with it the rows the test seeded before the
deadlock; nothing was committed. Without help, SQLAlchemy's next rollback to a
savepoint would then fail with "(1305, 'SAVEPOINT sa_savepoint_2 does not
exist')", which the application would see instead of the deadlock. So after a
deadlock (1213) pytest-sqla re-creates the savepoints SQLAlchemy still holds
before its next savepoint operation: the application sees 1213 as it would in
production, its rollback() works, a retry commits, and the guard watches
the rest of the test. With innodb_rollback_on_timeout = ON, a lock wait
timeout (1205) rolls the transaction back too, and pytest-sqla reports it as a
breach; that server setting is not tested.
MariaDB uses the same code path but is not tested in CI. Non-transactional tables (MyISAM, MEMORY) cannot be rolled back at all.
pytest-xdist
PostgreSQL and MySQL workers of one run share one database; SQLite workers
each get their own, a file per worker in create mode and an in-memory database
per process. Under the default isolation levels a worker never reads another
worker's rows, because no test commits; pytest-sqla refuses READ UNCOMMITTED
for that reason. The schema is created by the first worker under a database
lock (pg_advisory_xact_lock on PostgreSQL, GET_LOCK on MySQL), joined by
the others, and dropped by the last one out; on SQLite, BEGIN IMMEDIATE only
orders separate runs. Measured before this design: a create_all per worker
with -n 4 ended "2 passed, 6 errors" on every run against both servers.
Limits:
- Two workers inserting the same unique value wait on each other until one test ends, and two that insert the same values in opposite orders deadlock: measured on every run, with PostgreSQL's DeadlockDetected and MySQL's 1213. Make such values unique per test, or insert them in one order.
- One run per database: a second pytest run on the same database while the first is going gets SchemaConflictError. Per-run databases are on the roadmap.
- A worker that crashes never leaves, so its run's tables stay behind, a
per-worker SQLite file included, and the next run asks for
--sqla-recreate-schema. - Schema locks exist for PostgreSQL, MySQL, MariaDB and SQLite. Any other
dialect works in a single process; under xdist, create the schema beforehand
and set
sqla_schema = none.
What pytest-sqla reports
pytest-sqla never warns: suites run with filterwarnings = error, so every
problem is an error with a message that says what to do. Setup problems (no
URL, the wrong driver family, an async fixture in a sync test) fail the test at
setup. Three exception classes, all subclasses of pytest_sqla.PytestSqlaError:
- IsolationError, at the teardown of the test that broke isolation: it
committed the connection itself (
session.connection().commit()), the database ended the transaction on its own, or it committed on a second connection of pytest-sqla's engine (sqla_session.get_bind().engine.begin()). The same class reports a task the test left running on its connection, and a teardown that did not finish withinsqla_lock_timeout. A plainconnection.rollback()escapes nothing and is not an error, and neither is a MySQL deadlock. - LoopMismatchError, a
RuntimeErrortoo, at the first statement, COMMIT or ROLLBACK run on the wrong event loop. - SchemaConflictError, from
sqla_schema: on every database test for tables it did not create, another run's schema, or a lock that timed out during creation; once, at the end of the run, for a drop blocked by a leaked transaction.
A test's own failure always wins: a breach found at teardown is reported as a separate error and never replaces it. URLs in messages always hide the password.
Migrating
| From | Was | Now |
|---|---|---|
| pytest-async-sqlalchemy | --database-url, database_url |
--sqla-url, sqla_url |
db_session, dbsession |
sqla_async_session |
|
_database_url |
override sqla_url |
|
init_database |
sqla_metadata |
|
your event_loop fixture override |
delete it | |
| pytest-sqlalchemy | --sqlalchemy-connect-url |
--sqla-url |
connection, transaction |
sqla_connection |
|
dbsession |
sqla_session |
|
--sqlalchemy-manage-db, db_schema |
sqla_schema creates tables, not the database |
|
| sqlalchemyseed | your engine fixture, the SQLite listeners, StaticPool |
sqla_url; delete the rest |
sqlalchemyseed_session |
override it to return sqla_session, and keep seed |
pytest-sqla never creates or drops a database: pytest-sqlalchemy's db_schema
and pytest-async-sqlalchemy's database fixture did, per xdist worker in the
former's case. Create the database before the run, or supply it from a
container fixture as below; all workers of a run share it (per-worker
databases are on the roadmap).
Using it with database fixtures
sqla_url is an ordinary session fixture, so a container or server fixture
can supply it, as a str or a URL:
import pytest
from testcontainers.community.postgres import PostgresContainer
@pytest.fixture(scope="session")
def sqla_url():
with PostgresContainer("postgres:17-alpine", driver="psycopg") as postgres:
yield postgres.get_connection_url()
Everything that depends on sqla_url, the schema's drop included, is torn
down before the container stops. pytest-databases and pytest-postgresql
fixtures plug in the same way.
Limitations
- Code that builds its own engine or sessionmaker, instead of the one you inject, works outside the test transaction, and pytest-sqla cannot roll it back; it cannot even notice. Code that opens a second connection from pytest-sqla's own engine commits for real too, but that is reported.
- One connection per test: running two sessions concurrently, as with
asyncio.gather, fails as it would on one connection in production. Requests within one test must run one after another, and every task that uses the connection must be done before the test ends. - Isolation levels are the engine's: application code that asks for its own
per unit of work, as with
session.connection(execution_options={"isolation_level": "SERIALIZABLE"}), gets SQLAlchemy's InvalidRequestError, because the test's transaction has already begun. - Sequences and AUTO_INCREMENT counters are not transactional on PostgreSQL and MySQL: a rolled-back insert still uses up its id, so ids keep growing from test to test, while SQLite reuses them. Do not assert ids.
- Session state survives a rollback on a pooled connection: a
poolclassof your own, or a class, module, package or session loop, shares connections between tests (see Pooling). - CREATE SCHEMA for schema-qualified metadata is up to you.
Compatibility
- Python 3.11 to 3.15.
- SQLAlchemy 2.0 and 2.1; pytest 8.2 or newer; pytest-asyncio 1.2 or newer.
- Tested in CI on PostgreSQL 17 with psycopg 3 (sync and async) and asyncpg, on MySQL 8.4 with pymysql, aiomysql and asyncmy, and on SQLite with pysqlite and aiosqlite. MariaDB uses the MySQL code path and is untested.
Roadmap
Alembic as a schema mode, under the same lock; per-run and per-worker databases on PostgreSQL and MySQL; data seeded once per module inside an outer savepoint; rebinding an application's own sessionmaker; factory_boy and polyfactory helpers; a docs site.
License
MIT. pytest-sqla is not affiliated with the SQLAlchemy project.
Metadata
Release files for pytest-sqla 0.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_sqla-0.0.2.tar.gz | 119.7 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_sqla-0.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 167.7 kB
Release files / pytest_sqla-0.0.2.tar.gz
| Download URL | pytest_sqla-0.0.2.tar.gz |
|---|---|
| Size | 119.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5199b0e79b8e24a81969dc7447e23397599ff90013a71ea9bde4b3702f10a629
|
|
BLAKE2b-256 checksum How to use checksums |
82786cbafe981c90fe73783bc127b8d8e2ee2d41df9815644c0a5043404f5ccd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / pytest_sqla-0.0.2-py3-none-any.whl
| Download URL | pytest_sqla-0.0.2-py3-none-any.whl |
|---|---|
| Size | 48.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8bdc6b3d61fd353d6c25b4ad4ae0ea11e62ba9e27443a8e4411d1a5814bbbccc
|
|
BLAKE2b-256 checksum How to use checksums |
ecb2cca883ba209325d3450d8a85c723557b0fc7ec4f3541415137686246e4db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for pytest-sqla 0.0.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pytest_sqla-0.0.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.