SQLAlchemy transactions treat related changes as one unit: either every change is persisted with commit, or all of them are undone with rollback. A Session tracks ORM objects, implements the unit of work pattern, and checks out a connection when database communication begins.
Review the complete SQLAlchemy guide first if mappings and queries are new to you. The key design decision is to define a transaction boundary instead of tying the Session to the arbitrary lifetime of the application.
Frame a transaction with context managers
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine("postgresql+psycopg://app:password@localhost/app")
def transfer(source, destination, amount):
with Session(engine) as session:
with session.begin():
source.balance -= amount
destination.balance += amount
session.add_all([source, destination])
If the inner block finishes normally, session.begin() commits. If an exception occurs, it rolls back and propagates the error. The outer block closes the Session and releases resources. Do not catch an exception only to hide it, because callers need to know that the transfer failed.
A session factory makes this even shorter:
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(bind=engine)
with SessionLocal.begin() as session:
session.add(new_order)
flush is not commit
flush() emits pending INSERT, UPDATE, and DELETE statements while keeping the transaction open. It can obtain a generated key before creating a related object:
with SessionLocal.begin() as session:
order = Order(customer_id=42)
session.add(order)
session.flush()
session.add(Item(order_id=order.id, sku="PY-01"))
A query or commit() may trigger an automatic flush. If a constraint fails, the Session becomes inactive for that transaction. Call rollback() or let the context manager handle it before continuing.
Session scope in web applications
A common pattern opens one Session per request, runs business logic, commits writes, and closes it at the end. Do not share one Session instance across threads or asynchronous tasks. For async code, create one AsyncSession per task and read the async SQLAlchemy guide.
Avoid calling commit() inside every repository function. That makes a business operation with multiple steps impossible to keep atomic. Let the layer that understands the complete unit of work decide when to commit.
Recover one step with a savepoint
begin_nested() creates a savepoint when supported:
with SessionLocal.begin() as session:
session.add(batch)
try:
with session.begin_nested():
session.add(optional_item)
session.flush()
except IntegrityError:
record_rejected_item()
The error rolls back the savepoint without necessarily discarding the outer transaction. Use savepoints deliberately; they do not replace well-designed transaction boundaries.
Tests and common mistakes
Test both success and a failure in the middle of the operation, confirming that no partial state remains. Avoid global sessions, transactions held open during slow HTTP calls, and broad exception handlers that hide failures. Keep transactions short, but long enough to represent one indivisible business rule.
The official SQLAlchemy transaction documentation, accessed July 28, 2026, covers autobegin, context managers, savepoints, and isolation levels.