Async SQLAlchemy

SQLAlchemy · Advanced · 14 min

Async web frameworks like FastAPI need non-blocking database access, and async has rules the sync ORM doesn't.

The sqlalchemy.ext.asyncio extension wraps the same Core and ORM in an async API. Install it with pip install "sqlalchemy[asyncio]", which adds the greenlet dependency, plus an async DBAPI driver: postgresql+asyncpg://, mysql+aiomysql:// or sqlite+aiosqlite://.

  • engine = create_async_engine(url); async with engine.begin() as conn: for Core work, and await conn.run_sync(Base.metadata.create_all) to call sync-only helpers.
  • async_sessionmaker(engine, expire_on_commit=False) creates AsyncSessions. Disabling expire-on-commit matters more here, because reloading an expired attribute would require IO.
  • Every operation that talks to the database is awaited: await session.execute(stmt), await session.scalars(stmt), await session.get(User, 1), await session.commit(). session.add() is not awaited; it only stages.

The big rule: no implicit IO. Accessing a lazy-loaded relationship can't run a query from a plain attribute access, so it raises MissingGreenlet. Load what you need up front with selectinload, or use lazy="raise" to find mistakes; the AsyncAttrs mixin also offers await obj.awaitable_attrs.posts for an explicit async load.

One AsyncSession per task: it's not safe to use concurrently from multiple coroutines, so asyncio.gather over one session is a bug. Call await engine.dispose() at shutdown.

Example

import asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase): pass
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]

async def main():
    engine = create_async_engine("sqlite+aiosqlite://")
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    Session = async_sessionmaker(engine, expire_on_commit=False)
    async with Session() as s:
        s.add_all([User(name="ada"), User(name="bob")])   # no await: just staging
        await s.commit()
        names = (await s.scalars(select(User.name).order_by(User.name))).all()
        print(names)                                        # ['ada', 'bob']
    await engine.dispose()

asyncio.run(main())

Remember

  • Async engine + async driver (asyncpg, aiosqlite) + AsyncSession
  • Await execute/scalars/get/commit; add() is synchronous
  • No lazy loading: eager-load relationships or use awaitable_attrs
  • One AsyncSession per task; never share it across concurrent coroutines

Watch out for

  • Touching user.posts in async code and getting MissingGreenlet.
  • Running asyncio.gather on several queries using the same AsyncSession.

Check yourself

  1. Why is expire_on_commit=False usually recommended for AsyncSession?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html