SQLAlchemy DetachedInstanceError: not bound to a Session (tested)
DetachedInstanceError: Parent instance is not bound to a Session; lazy load operation of attribute cannot proceed means you touched a relationship (or an attribute expired by commit()) after the object's session closed. Load it up front with selectinload(), keep values with expire_on_commit=False, or re-attach with session.merge().
Every block imports this common.py (in-memory SQLite, one author with two books):
from sqlalchemy import create_engine, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class Author(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
books: Mapped[list["Book"]] = relationship(back_populates="author")
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
author: Mapped[Author] = relationship(back_populates="books")
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as s:
s.add(Author(name="Ada", books=[Book(title="Notes"), Book(title="Engines")])); s.commit()
1. The error: lazy loading after the session closed
from common import *
from sqlalchemy import select
def get_author():
with Session(engine) as s:
return s.scalars(select(Author)).first()
a = get_author()
print(a.name)
print(a.books) # lazy-loaded relationship, but the session is closed
Output:
Ada
Traceback (most recent call last):
File "/w/b1.py", line 8, in <module>
print(a.books) # lazy-loaded relationship, but the session is closed
^^^^^^^
File "/usr/local/lib/python3.14/site-packages/sqlalchemy/orm/attributes.py", line 575, in __get__
return self.impl.get(state, dict_) # type: ignore[no-any-return]
~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/site-packages/sqlalchemy/orm/attributes.py", line 1091, in get
value = self._fire_loader_callables(state, key, passive)
File "/usr/local/lib/python3.14/site-packages/sqlalchemy/orm/attributes.py", line 1126, in _fire_loader_callables
return self.callable_(state, passive)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/site-packages/sqlalchemy/orm/strategies.py", line 957, in _load_for_state
raise orm_exc.DetachedInstanceError(
...<3 lines>...
)
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <Author at 0x7f46085a3d90> is not bound to a Session; lazy load operation of attribute 'books' cannot proceed (Background on this error at: https://sqlalche.me/e/21/bhk3)
a.name was loaded by the query, so it works. a.books is a lazy relationship: SQLAlchemy runs a second query the first time you touch it, and that needs the session. The session closed when the function returned, so the object is detached. This is the classic case: a repository function or a request handler returns ORM objects, and a template or serializer touches a relationship later.
2. After commit, even plain columns fail
from common import *
from sqlalchemy import select
with Session(engine) as s:
a = s.scalars(select(Author)).first()
s.commit() # expire_on_commit=True expires every attribute
try:
print(a.name) # even a plain column now needs the session
except Exception as e:
print(type(e).__name__ + ":", str(e)[:120])
Output:
DetachedInstanceError: Instance <Author at 0x7f43a311ae90> is not bound to a Session; attribute refresh operation cannot proceed (Background on
By default commit() expires every loaded attribute (expire_on_commit=True), so the next access must refresh from the database. If the session is gone by then, you get the variant attribute refresh operation cannot proceed.
3. Fix: load what you need before the session closes
from common import *
from sqlalchemy import select
from sqlalchemy.orm import selectinload
def get_author():
with Session(engine) as s:
return s.scalars(select(Author).options(selectinload(Author.books))).first()
a = get_author()
print(a.name, [b.title for b in a.books]) # loaded up front, works after close
Output:
Ada ['Notes', 'Engines']
selectinload() (or joinedload()) fetches the relationship in the original query, so nothing is lazy afterwards. This is the cleanest fix: it also avoids the N+1 queries that lazy loading causes in loops. Converting to plain dicts or Pydantic models inside the session works for the same reason.
4. expire_on_commit=False keeps loaded values readable
from common import *
from sqlalchemy import select
with Session(engine, expire_on_commit=False) as s:
a = s.scalars(select(Author)).first()
a.name = "Ada L."
s.commit()
print(a.name) # still readable after commit + close
try:
a.books
except Exception as e:
print("unloaded relationship still fails:", type(e).__name__)
Output:
Ada L.
unloaded relationship still fails: DetachedInstanceError
This fixes block 2 (values stay as they were at commit), but not block 1: a relationship that was never loaded still needs a session. Many web apps set sessionmaker(..., expire_on_commit=False); it is the recommended setting with AsyncSession.
5. Re-attach a detached object with merge()
from common import *
from sqlalchemy import select
with Session(engine) as s:
a = s.scalars(select(Author)).first()
with Session(engine) as s2:
a = s2.merge(a) # re-attach to a new session
print(a.name, [b.title for b in a.books])
Output:
Ada ['Notes', 'Engines']
session.merge(obj) returns a copy that belongs to the new session, so lazy loads work again. Use the returned object, not the original.
Related: SQLAlchemy IntegrityError and PendingRollbackError ยท SQLite database is locked.
Frequently asked questions
What causes DetachedInstanceError?
Accessing a lazy relationship, or any attribute expired by commit(), on an object whose session has been closed. SQLAlchemy needs the session to run the load query.
Does expire_on_commit=False fix DetachedInstanceError?
Only for attributes that were already loaded. Relationships that were never loaded still fail; use selectinload() or joinedload() in the query.
How do I use an object in a new session?
obj = new_session.merge(obj), then use the returned object.
Written with AI assistance. Measured on 2026-10-23 with SQLAlchemy 2.1.6 and Python 3.14.7 (python:3-alpine in Docker), SQLite in-memory database. Each block was run verbatim and the output pasted unedited. PlainTables is our extension; Python and SQLAlchemy are free.