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.