Minimalistic context-local session and transaction controller for SQLAlchemy AsyncSession.
sqlalchemy-tx-context provides context-aware session and transaction management using Python’s contextvars,
eliminating the need to pass AsyncSession objects explicitly. Especially useful in code where database access should
be decoupled from explicit session passing - such as service layers or background jobs.
- Context-local session management via
contextvars, without relying on thread-locals or global session objects. - Clean
async withAPI for managing session and transaction scopes. - Supports safe nesting of transactions.
.execute(...)automatically creates a session or transaction if none is active (optional fallback).- Explicit control over how sessions are scoped and reused.
pip install sqlalchemy-tx-contextfrom sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy_tx_context import SQLAlchemyTransactionContext
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
db = SQLAlchemyTransactionContext(engine)
async def create_user():
async with db.transaction():
await db.execute(insert(User).values(name="John"))
async def get_users():
async with db.session():
result = await db.execute(select(User))
return result.scalars().all()SQLAlchemyTransactionContext(
engine: AsyncEngine,
*,
default_session_maker: Optional[async_sessionmaker[AsyncSession]] = None,
auto_context_on_execute: bool = False,
auto_context_force_transaction: bool = False,
)engine- SQLAlchemyAsyncEngineinstance.default_session_maker- Optional async session factory. If omitted, usesasync_sessionmaker(engine).auto_context_on_execute- IfTrue, allows.execute()to run even without an active session by creating a temporary one.auto_context_force_transaction- IfTrue,.execute()always runs inside a transaction when auto context is used. IfFalse, it uses.session()for read-only queries (likeSelectorCompoundSelect), and.transaction()for everything else, includingInsert,Update, and raw SQL.
session(...) -> AsyncIterator[AsyncSession]- Enter a new session context, or reuse an existing one ifreuse_if_exists=True.transaction(...) -> AsyncIterator[AsyncSession]- Enter a transactional context. Will nest if a transaction is already active.new_session(...) -> AsyncIterator[AsyncSession]- Create a new isolated session, even if another is already active. Overrides the context for the duration.new_transaction(...) -> AsyncIterator[AsyncSession]- Create a new transaction in an isolated session.get_session(strict: bool = True) -> AsyncSession | None- Return the current session from context. RaisesNoSessionErrorifstrict=Trueand no session exists.execute(...) -> Result- Execute a SQLAlchemyExecutableusing the current or temporary context. Uses the current session if one is active. Otherwise, behavior depends onauto_context_on_execute- a new session or transaction context may be created automatically.
# Opens a temporary session or transaction depending on statement type
db = SQLAlchemyTransactionContext(engine, auto_context_on_execute=True)
await db.execute(insert(User).values(name="Alice"))
await db.execute(select(User))For a complete working example using PostgreSQL, see
example/.
It demonstrates table creation, data insertion, transactional rollback, and querying.
This library defines a few custom exceptions to help catch context-related mistakes:
NoSessionError: Raised when attempting to access or use a session when none is active and fallback is disabled.SessionAlreadyActiveError: Raised when entering.session()while a session is already active (unlessreuse_if_exists=True).TransactionAlreadyActiveError: Raised when entering.transaction()while a transaction is already active and nesting is disabled.
SQLAlchemy does not provide an out-of-the-box solution for context-local session tracking when working with
AsyncSession. Passing sessions around explicitly can make service-layer code verbose and harder to maintain.
The library introduces a lightweight and predictable abstraction that:
- Stores current session in a
ContextVar. - Provides safe transactional/session boundaries.
- Exposes a unified
execute(...)interface. - Integrates with standard SQLAlchemy models, statements, and engines.
- You're writing service-layer logic and want to avoid passing
sessionexplicitly. - You need nested transactional logic with clean context boundaries.
- You prefer explicit context management over dependency-injected sessions.
- You work with context-local transaction boundaries in background tasks or microservices.
- You need precise control over session lifecycle and scope.
This library is best suited for functional or script-style code where sessions are not injected via DI frameworks.
- Use
.transaction()or.session()explicitly in your service-layer or job code to clearly define execution boundaries. .execute(...)is ideal for small projects, functional-style code, or early-stage prototypes - it avoids boilerplate and makes code fast to write.- As your project grows, you can gradually migrate to DI-based session and transaction management:
just replace
db.execute(...)withself._session.execute(...)anddb.transaction()with your own context (e.g.,UnitOfWork,session.begin()) - the query logic remains the same. - This makes the library especially useful for bootstrapping or background scripts, where a full-blown DI setup would be overkill.
pytest --cov=sqlalchemy_tx_context --cov-report=term-missingTested on Python 3.9 - 3.12.
MIT License