Async repository pattern with SQLAlchemy 2 AsyncSession on aiosqlite: working versions (tested 2026-10-15)
A typed async repository over SQLAlchemy 2 AsyncSession with eager loading, foreign keys enforced on SQLite, and offline pytest-asyncio tests against an in-memory database.
passed6 passed · 0 failed2026-10-15 13:33 UTC- Status
- passed
- Last passed
- 2026-10-15 13:33 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sqlalchemy-async-repository-pattern") (install), or see them on GitHub.
sqlalchemy==2.1.4 aiosqlite==0.22.1 greenlet==3.2.4 pytest-asyncio==1.4.0 pytest==9.1.1
from sqlalchemy import ForeignKey, String, event
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, selectinload
from sqlalchemy import select
from sqlalchemy.pool import StaticPool
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
books: Mapped[list["Book"]] = relationship(back_populates="author", cascade="all, delete-orphan")
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
author: Mapped[Author] = relationship(back_populates="books")
def make_engine(url: str = "sqlite+aiosqlite:///:memory:") -> AsyncEngine:
# StaticPool keeps one connection, so every session sees the same in-memory database.
engine = create_async_engine(url, poolclass=StaticPool)
@event.listens_for(engine.sync_engine, "connect")
def _enable_fk(dbapi_conn, _record):
# SQLite ignores foreign keys unless enabled on each connection.
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
return engine
def make_sessionmaker(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
# expire_on_commit=False keeps loaded attributes readable after commit,
# so async code does not trigger implicit IO.
return async_sessionmaker(engine, expire_on_commit=False)
class AuthorRepo:
def __init__(self, session: AsyncSession):
self.session = session
async def add(self, name: str) -> Author:
author = Author(name=name)
self.session.add(author)
await self.session.flush() # assigns author.id without committing
return author
async def add_book(self, author_id: int, title: str) -> Book:
book = Book(title=title, author_id=author_id)
self.session.add(book)
await self.session.flush()
return book
async def get_with_books(self, author_id: int) -> Author | None:
stmt = select(Author).options(selectinload(Author.books)).where(Author.id == author_id)
return (await self.session.execute(stmt)).scalar_one_or_none()
async def count(self) -> int:
return len((await self.session.execute(select(Author))).scalars().all())
import pytest
import pytest_asyncio
from sqlalchemy.exc import IntegrityError, MissingGreenlet, StatementError
from repository import Author, AuthorRepo, Base, make_engine, make_sessionmaker
@pytest_asyncio.fixture
async def engine():
eng = make_engine()
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield eng
await eng.dispose()
@pytest.fixture
def maker(engine):
return make_sessionmaker(engine)
@pytest.mark.asyncio
async def test_add_and_fetch_books_with_selectinload(maker):
async with maker() as s:
repo = AuthorRepo(s)
author = await repo.add("Ursula")
await repo.add_book(author.id, "The Dispossessed")
await s.commit()
async with maker() as s:
found = await AuthorRepo(s).get_with_books(author.id)
assert [b.title for b in found.books] == ["The Dispossessed"]
@pytest.mark.asyncio
async def test_lazy_load_raises_in_async(maker):
async with maker() as s:
author = await AuthorRepo(s).add("Octavia")
await s.commit()
async with maker() as s:
loaded = await s.get(Author, author.id)
# Lazy loading needs IO, which is impossible inside a sync attribute access.
# SQLAlchemy wraps the MissingGreenlet in a StatementError, so check the original.
with pytest.raises(StatementError) as info:
_ = loaded.books
assert isinstance(info.value.orig, MissingGreenlet)
@pytest.mark.asyncio
async def test_foreign_key_enforced(maker):
async with maker() as s:
repo = AuthorRepo(s)
with pytest.raises(IntegrityError):
await repo.add_book(999, "Orphan")
@pytest.mark.asyncio
async def test_unique_name_raises(maker):
async with maker() as s:
await AuthorRepo(s).add("Dup")
await s.commit()
async with maker() as s:
with pytest.raises(IntegrityError):
await AuthorRepo(s).add("Dup")
@pytest.mark.asyncio
async def test_rollback_discards_changes(maker):
async with maker() as s:
await AuthorRepo(s).add("Ghost")
await s.rollback()
async with maker() as s:
assert await AuthorRepo(s).count() == 0
@pytest.mark.asyncio
async def test_attributes_readable_after_commit(maker):
async with maker() as s:
author = await AuthorRepo(s).add("Le Guin")
await s.commit()
# expire_on_commit=False: no lazy refresh, so no await is needed here.
assert author.name == "Le Guin"
[pytest] pythonpath = . asyncio_default_fixture_loop_scope = function
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI with async SQLAlchemy 2 on aiosqlite, tested offline with per-test in-memory DBs
A FastAPI app using AsyncSession and async_sessionmaker on aiosqlite, tested with httpx.AsyncClient over ASGITransport, overriding the session dependency with a fresh in-memory database for each test.
SQLAlchemy 2 async ORM + aiosqlite with pytest-asyncio
SQLAlchemy 2.x typed async ORM (Mapped, mapped_column, async_sessionmaker, selectinload) on aiosqlite, with pytest-asyncio fixtures.
Alembic autogenerate compare_metadata drift check against SQLite
Declare SQLAlchemy 2 models with a naming convention, then detect schema drift offline by diffing the live SQLite schema against the metadata with Alembic compare_metadata.
asyncio TaskGroup, timeouts and semaphores tested with pytest-asyncio
Bounded concurrent fetches with asyncio.Semaphore, per-call asyncio.timeout and TaskGroup error handling, tested offline with pytest-asyncio loop scope config and async fixtures.
All SQLAlchemy recipesAll aiosqlite recipesAll pytest-asyncio recipes