Short answer
The Session applies the Unit of Work and Identity Map patterns: it tracks loaded and modified objects and writes them all at flush or commit inside a transaction. A session should live for a single request or task only because it is not thread-safe and holds state.
Explanation
The Identity Map guarantees a single object per row within the session. commit ends the transaction and expires the objects (expire_on_commit) so they are reloaded on next access. A global session shared across requests causes data leaks and concurrency bugs. In SQLAlchemy 2.0 the unified form is select() with session.execute and scalars, and Mapped and mapped_column for types. In async code use AsyncSession and note that lazy loading of relations does not work implicitly, so load them with selectinload.
Code example
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
def rename_user(engine, user_id: int, name: str) -> None:
with Session(engine) as session, session.begin(): # commit Or automatic rollback
user = session.get(User, user_id)
user.name = name # Automatically tracked
def list_authors(session: Session):
stmt = select(Author).options(selectinload(Author.books)).where(Author.active)
return session.scalars(stmt).all()Common mistakes
- Using one global session across the application for all requests.
- Accessing a lazy relation after the session is closed, which raises DetachedInstanceError.