[{"data":1,"prerenderedAt":2456},["ShallowReactive",2],{"page-\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002Ffixing-detachedinstanceerror-after-commit-in-sqlalchemy\u002F":3},{"id":4,"title":5,"body":6,"description":2448,"extension":2449,"meta":2450,"navigation":130,"path":2452,"seo":2453,"stem":2454,"__hash__":2455},"content\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002Ffixing-detachedinstanceerror-after-commit-in-sqlalchemy\u002Findex.md","Fixing DetachedInstanceError After Commit in SQLAlchemy",{"type":7,"value":8,"toc":2433},"minimark",[9,13,44,49,68,71,237,381,460,464,467,1190,1193,1228,1231,1235,1411,1416,1422,1641,1645,1652,1658,1962,1968,1976,1985,2262,2278,2282,2298,2332,2364,2387,2391,2429],[10,11,5],"h1",{"id":12},"fixing-detachedinstanceerror-after-commit-in-sqlalchemy",[14,15,16,17,21,22,25,26,29,30,33,34,37,38,43],"p",{},"Set ",[18,19,20],"code",{},"expire_on_commit=False"," on your ",[18,23,24],{},"sessionmaker"," or ",[18,27,28],{},"async_sessionmaker"," factory — this is the fastest fix for ",[18,31,32],{},"DetachedInstanceError"," on attribute access after ",[18,35,36],{},"commit()",", and it is covered in depth in the ",[39,40,42],"a",{"href":41},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002F","Session Lifecycle and Scope Management"," guide alongside all other object state transitions.",[45,46,48],"h2",{"id":47},"quick-answer","Quick Answer",[14,50,51,53,54,56,57,60,61,64,65,67],{},[18,52,32],{}," after ",[18,55,36],{}," occurs because SQLAlchemy expires every attribute on every tracked object at commit time (",[18,58,59],{},"expire_on_commit=True"," is the default). If the session is then closed before you access those attributes, the ORM has nowhere to issue the ",[18,62,63],{},"SELECT"," that would reload them, and it raises ",[18,66,32],{},".",[14,69,70],{},"The three production fixes, in order of preference:",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"language-python shiki shiki-themes github-light github-dark","# Fix 1: Disable expiry on commit (preferred for APIs)\n# Before (causes DetachedInstanceError if session closes before attribute access):\nsession_factory = sessionmaker(engine, expire_on_commit=True)  # default\n\n# After (attributes remain loaded after commit):\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\", pool_size=10)\nAsyncSessionLocal = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,  # attributes survive commit without re-SELECT\n)\n","python","",[18,79,80,89,95,125,132,138,153,158,187,198,204,215,232],{"__ignoreMap":77},[81,82,85],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"sJ8bj","# Fix 1: Disable expiry on commit (preferred for APIs)\n",[81,90,92],{"class":83,"line":91},2,[81,93,94],{"class":87},"# Before (causes DetachedInstanceError if session closes before attribute access):\n",[81,96,98,102,106,109,113,115,119,122],{"class":83,"line":97},3,[81,99,101],{"class":100},"sVt8B","session_factory ",[81,103,105],{"class":104},"szBVR","=",[81,107,108],{"class":100}," sessionmaker(engine, ",[81,110,112],{"class":111},"s4XuR","expire_on_commit",[81,114,105],{"class":104},[81,116,118],{"class":117},"sj4cs","True",[81,120,121],{"class":100},")  ",[81,123,124],{"class":87},"# default\n",[81,126,128],{"class":83,"line":127},4,[81,129,131],{"emptyLinePlaceholder":130},true,"\n",[81,133,135],{"class":83,"line":134},5,[81,136,137],{"class":87},"# After (attributes remain loaded after commit):\n",[81,139,141,144,147,150],{"class":83,"line":140},6,[81,142,143],{"class":104},"from",[81,145,146],{"class":100}," sqlalchemy.ext.asyncio ",[81,148,149],{"class":104},"import",[81,151,152],{"class":100}," AsyncSession, async_sessionmaker, create_async_engine\n",[81,154,156],{"class":83,"line":155},7,[81,157,131],{"emptyLinePlaceholder":130},[81,159,161,164,166,169,173,176,179,181,184],{"class":83,"line":160},8,[81,162,163],{"class":100},"engine ",[81,165,105],{"class":104},[81,167,168],{"class":100}," create_async_engine(",[81,170,172],{"class":171},"sZZnC","\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\"",[81,174,175],{"class":100},", ",[81,177,178],{"class":111},"pool_size",[81,180,105],{"class":104},[81,182,183],{"class":117},"10",[81,185,186],{"class":100},")\n",[81,188,190,193,195],{"class":83,"line":189},9,[81,191,192],{"class":100},"AsyncSessionLocal ",[81,194,105],{"class":104},[81,196,197],{"class":100}," async_sessionmaker(\n",[81,199,201],{"class":83,"line":200},10,[81,202,203],{"class":100},"    engine,\n",[81,205,207,210,212],{"class":83,"line":206},11,[81,208,209],{"class":111},"    class_",[81,211,105],{"class":104},[81,213,214],{"class":100},"AsyncSession,\n",[81,216,218,221,223,226,229],{"class":83,"line":217},12,[81,219,220],{"class":111},"    expire_on_commit",[81,222,105],{"class":104},[81,224,225],{"class":117},"False",[81,227,228],{"class":100},",  ",[81,230,231],{"class":87},"# attributes survive commit without re-SELECT\n",[81,233,235],{"class":83,"line":234},13,[81,236,186],{"class":100},[72,238,240],{"className":74,"code":239,"language":76,"meta":77,"style":77},"# Fix 2: Eager-load attributes you need before the session closes\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import selectinload\n\nasync def get_order_with_items(session: AsyncSession, order_id: int):\n    stmt = (\n        select(Order)\n        .where(Order.id == order_id)\n        .options(selectinload(Order.items))  # hydrates before commit\n    )\n    result = await session.execute(stmt)\n    order = result.scalar_one()\n    await session.commit()\n    # order.items is safe — already loaded into memory\n    return order\n",[18,241,242,247,259,271,275,296,306,311,322,330,335,348,358,366,372],{"__ignoreMap":77},[81,243,244],{"class":83,"line":84},[81,245,246],{"class":87},"# Fix 2: Eager-load attributes you need before the session closes\n",[81,248,249,251,254,256],{"class":83,"line":91},[81,250,143],{"class":104},[81,252,253],{"class":100}," sqlalchemy ",[81,255,149],{"class":104},[81,257,258],{"class":100}," select\n",[81,260,261,263,266,268],{"class":83,"line":97},[81,262,143],{"class":104},[81,264,265],{"class":100}," sqlalchemy.orm ",[81,267,149],{"class":104},[81,269,270],{"class":100}," selectinload\n",[81,272,273],{"class":83,"line":127},[81,274,131],{"emptyLinePlaceholder":130},[81,276,277,280,283,287,290,293],{"class":83,"line":134},[81,278,279],{"class":104},"async",[81,281,282],{"class":104}," def",[81,284,286],{"class":285},"sScJk"," get_order_with_items",[81,288,289],{"class":100},"(session: AsyncSession, order_id: ",[81,291,292],{"class":117},"int",[81,294,295],{"class":100},"):\n",[81,297,298,301,303],{"class":83,"line":140},[81,299,300],{"class":100},"    stmt ",[81,302,105],{"class":104},[81,304,305],{"class":100}," (\n",[81,307,308],{"class":83,"line":155},[81,309,310],{"class":100},"        select(Order)\n",[81,312,313,316,319],{"class":83,"line":160},[81,314,315],{"class":100},"        .where(Order.id ",[81,317,318],{"class":104},"==",[81,320,321],{"class":100}," order_id)\n",[81,323,324,327],{"class":83,"line":189},[81,325,326],{"class":100},"        .options(selectinload(Order.items))  ",[81,328,329],{"class":87},"# hydrates before commit\n",[81,331,332],{"class":83,"line":200},[81,333,334],{"class":100},"    )\n",[81,336,337,340,342,345],{"class":83,"line":206},[81,338,339],{"class":100},"    result ",[81,341,105],{"class":104},[81,343,344],{"class":104}," await",[81,346,347],{"class":100}," session.execute(stmt)\n",[81,349,350,353,355],{"class":83,"line":217},[81,351,352],{"class":100},"    order ",[81,354,105],{"class":104},[81,356,357],{"class":100}," result.scalar_one()\n",[81,359,360,363],{"class":83,"line":234},[81,361,362],{"class":104},"    await",[81,364,365],{"class":100}," session.commit()\n",[81,367,369],{"class":83,"line":368},14,[81,370,371],{"class":87},"    # order.items is safe — already loaded into memory\n",[81,373,375,378],{"class":83,"line":374},15,[81,376,377],{"class":104},"    return",[81,379,380],{"class":100}," order\n",[72,382,384],{"className":74,"code":383,"language":76,"meta":77,"style":77},"# Fix 3: Refresh the specific object before session closes\nasync def create_user(session: AsyncSession, name: str) -> User:\n    user = User(name=name)\n    session.add(user)\n    await session.commit()\n    await session.refresh(user)  # re-SELECTs all columns from DB\n    # Now user.id, user.created_at, and all columns are re-loaded\n    return user\n",[18,385,386,391,409,427,432,438,448,453],{"__ignoreMap":77},[81,387,388],{"class":83,"line":84},[81,389,390],{"class":87},"# Fix 3: Refresh the specific object before session closes\n",[81,392,393,395,397,400,403,406],{"class":83,"line":91},[81,394,279],{"class":104},[81,396,282],{"class":104},[81,398,399],{"class":285}," create_user",[81,401,402],{"class":100},"(session: AsyncSession, name: ",[81,404,405],{"class":117},"str",[81,407,408],{"class":100},") -> User:\n",[81,410,411,414,416,419,422,424],{"class":83,"line":97},[81,412,413],{"class":100},"    user ",[81,415,105],{"class":104},[81,417,418],{"class":100}," User(",[81,420,421],{"class":111},"name",[81,423,105],{"class":104},[81,425,426],{"class":100},"name)\n",[81,428,429],{"class":83,"line":127},[81,430,431],{"class":100},"    session.add(user)\n",[81,433,434,436],{"class":83,"line":134},[81,435,362],{"class":104},[81,437,365],{"class":100},[81,439,440,442,445],{"class":83,"line":140},[81,441,362],{"class":104},[81,443,444],{"class":100}," session.refresh(user)  ",[81,446,447],{"class":87},"# re-SELECTs all columns from DB\n",[81,449,450],{"class":83,"line":155},[81,451,452],{"class":87},"    # Now user.id, user.created_at, and all columns are re-loaded\n",[81,454,455,457],{"class":83,"line":160},[81,456,377],{"class":104},[81,458,459],{"class":100}," user\n",[45,461,463],{"id":462},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[14,465,466],{},"The error appears most frequently in async FastAPI applications because the typical dependency pattern yields the session to the route handler, commits on exit, and then closes the session — all before the response serializer accesses the returned ORM object.",[72,468,470],{"className":74,"code":469,"language":76,"meta":77,"style":77},"from __future__ import annotations\n\nfrom typing import AsyncGenerator\nfrom fastapi import Depends, FastAPI\nfrom sqlalchemy import Integer, String, select\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\napp = FastAPI()\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = \"users\"\n    id: Mapped[int] = mapped_column(Integer, primary_key=True)\n    name: Mapped[str] = mapped_column(String(100))\n    email: Mapped[str] = mapped_column(String(255))\n\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\", pool_size=10)\n\n# BROKEN: expire_on_commit=True (default)\nBrokenSessionLocal = async_sessionmaker(engine, class_=AsyncSession)\n\n# FIXED: expire_on_commit=False\nFixedSessionLocal = async_sessionmaker(\n    engine, class_=AsyncSession, expire_on_commit=False\n)\n\n\nasync def get_broken_session() -> AsyncGenerator[AsyncSession, None]:\n    async with BrokenSessionLocal() as session:\n        yield session\n        await session.commit()\n        # session closes here — all attributes expired\n\n\nasync def get_fixed_session() -> AsyncGenerator[AsyncSession, None]:\n    async with FixedSessionLocal() as session:\n        yield session\n        await session.commit()\n        # attributes remain in-memory after commit\n\n\n@app.post(\"\u002Fusers-broken\")\nasync def create_user_broken(\n    name: str, session: AsyncSession = Depends(get_broken_session)\n) -> dict:\n    user = User(name=name, email=f\"{name}@example.com\")\n    session.add(user)\n    # commit happens in dependency teardown, AFTER this function returns\n    # Pydantic tries to access user.id AFTER the session is closed\n    return {\"id\": user.id, \"name\": user.name}  # DetachedInstanceError here\n\n\n@app.post(\"\u002Fusers-fixed\")\nasync def create_user_fixed(\n    name: str, session: AsyncSession = Depends(get_fixed_session)\n) -> dict:\n    user = User(name=name, email=f\"{name}@example.com\")\n    session.add(user)\n    # expire_on_commit=False keeps attributes loaded after dependency teardown\n    return {\"id\": user.id, \"name\": user.name}  # Safe\n",[18,471,472,485,489,501,513,524,535,545,549,559,563,567,583,588,592,596,611,622,650,671,690,695,700,721,726,732,751,756,762,772,792,797,802,807,826,844,853,861,867,872,877,893,907,914,921,927,932,937,950,963,979,991,1031,1036,1042,1048,1071,1076,1081,1093,1105,1119,1128,1161,1166,1172],{"__ignoreMap":77},[81,473,474,476,479,482],{"class":83,"line":84},[81,475,143],{"class":104},[81,477,478],{"class":117}," __future__",[81,480,481],{"class":104}," import",[81,483,484],{"class":100}," annotations\n",[81,486,487],{"class":83,"line":91},[81,488,131],{"emptyLinePlaceholder":130},[81,490,491,493,496,498],{"class":83,"line":97},[81,492,143],{"class":104},[81,494,495],{"class":100}," typing ",[81,497,149],{"class":104},[81,499,500],{"class":100}," AsyncGenerator\n",[81,502,503,505,508,510],{"class":83,"line":127},[81,504,143],{"class":104},[81,506,507],{"class":100}," fastapi ",[81,509,149],{"class":104},[81,511,512],{"class":100}," Depends, FastAPI\n",[81,514,515,517,519,521],{"class":83,"line":134},[81,516,143],{"class":104},[81,518,253],{"class":100},[81,520,149],{"class":104},[81,522,523],{"class":100}," Integer, String, select\n",[81,525,526,528,530,532],{"class":83,"line":140},[81,527,143],{"class":104},[81,529,265],{"class":100},[81,531,149],{"class":104},[81,533,534],{"class":100}," DeclarativeBase, Mapped, mapped_column\n",[81,536,537,539,541,543],{"class":83,"line":155},[81,538,143],{"class":104},[81,540,146],{"class":100},[81,542,149],{"class":104},[81,544,152],{"class":100},[81,546,547],{"class":83,"line":160},[81,548,131],{"emptyLinePlaceholder":130},[81,550,551,554,556],{"class":83,"line":189},[81,552,553],{"class":100},"app ",[81,555,105],{"class":104},[81,557,558],{"class":100}," FastAPI()\n",[81,560,561],{"class":83,"line":200},[81,562,131],{"emptyLinePlaceholder":130},[81,564,565],{"class":83,"line":206},[81,566,131],{"emptyLinePlaceholder":130},[81,568,569,572,575,578,581],{"class":83,"line":217},[81,570,571],{"class":104},"class",[81,573,574],{"class":285}," Base",[81,576,577],{"class":100},"(",[81,579,580],{"class":285},"DeclarativeBase",[81,582,295],{"class":100},[81,584,585],{"class":83,"line":234},[81,586,587],{"class":104},"    pass\n",[81,589,590],{"class":83,"line":368},[81,591,131],{"emptyLinePlaceholder":130},[81,593,594],{"class":83,"line":374},[81,595,131],{"emptyLinePlaceholder":130},[81,597,599,601,604,606,609],{"class":83,"line":598},16,[81,600,571],{"class":104},[81,602,603],{"class":285}," User",[81,605,577],{"class":100},[81,607,608],{"class":285},"Base",[81,610,295],{"class":100},[81,612,614,617,619],{"class":83,"line":613},17,[81,615,616],{"class":100},"    __tablename__ ",[81,618,105],{"class":104},[81,620,621],{"class":171}," \"users\"\n",[81,623,625,628,631,633,636,638,641,644,646,648],{"class":83,"line":624},18,[81,626,627],{"class":117},"    id",[81,629,630],{"class":100},": Mapped[",[81,632,292],{"class":117},[81,634,635],{"class":100},"] ",[81,637,105],{"class":104},[81,639,640],{"class":100}," mapped_column(Integer, ",[81,642,643],{"class":111},"primary_key",[81,645,105],{"class":104},[81,647,118],{"class":117},[81,649,186],{"class":100},[81,651,653,656,658,660,662,665,668],{"class":83,"line":652},19,[81,654,655],{"class":100},"    name: Mapped[",[81,657,405],{"class":117},[81,659,635],{"class":100},[81,661,105],{"class":104},[81,663,664],{"class":100}," mapped_column(String(",[81,666,667],{"class":117},"100",[81,669,670],{"class":100},"))\n",[81,672,674,677,679,681,683,685,688],{"class":83,"line":673},20,[81,675,676],{"class":100},"    email: Mapped[",[81,678,405],{"class":117},[81,680,635],{"class":100},[81,682,105],{"class":104},[81,684,664],{"class":100},[81,686,687],{"class":117},"255",[81,689,670],{"class":100},[81,691,693],{"class":83,"line":692},21,[81,694,131],{"emptyLinePlaceholder":130},[81,696,698],{"class":83,"line":697},22,[81,699,131],{"emptyLinePlaceholder":130},[81,701,703,705,707,709,711,713,715,717,719],{"class":83,"line":702},23,[81,704,163],{"class":100},[81,706,105],{"class":104},[81,708,168],{"class":100},[81,710,172],{"class":171},[81,712,175],{"class":100},[81,714,178],{"class":111},[81,716,105],{"class":104},[81,718,183],{"class":117},[81,720,186],{"class":100},[81,722,724],{"class":83,"line":723},24,[81,725,131],{"emptyLinePlaceholder":130},[81,727,729],{"class":83,"line":728},25,[81,730,731],{"class":87},"# BROKEN: expire_on_commit=True (default)\n",[81,733,735,738,740,743,746,748],{"class":83,"line":734},26,[81,736,737],{"class":100},"BrokenSessionLocal ",[81,739,105],{"class":104},[81,741,742],{"class":100}," async_sessionmaker(engine, ",[81,744,745],{"class":111},"class_",[81,747,105],{"class":104},[81,749,750],{"class":100},"AsyncSession)\n",[81,752,754],{"class":83,"line":753},27,[81,755,131],{"emptyLinePlaceholder":130},[81,757,759],{"class":83,"line":758},28,[81,760,761],{"class":87},"# FIXED: expire_on_commit=False\n",[81,763,765,768,770],{"class":83,"line":764},29,[81,766,767],{"class":100},"FixedSessionLocal ",[81,769,105],{"class":104},[81,771,197],{"class":100},[81,773,775,778,780,782,785,787,789],{"class":83,"line":774},30,[81,776,777],{"class":100},"    engine, ",[81,779,745],{"class":111},[81,781,105],{"class":104},[81,783,784],{"class":100},"AsyncSession, ",[81,786,112],{"class":111},[81,788,105],{"class":104},[81,790,791],{"class":117},"False\n",[81,793,795],{"class":83,"line":794},31,[81,796,186],{"class":100},[81,798,800],{"class":83,"line":799},32,[81,801,131],{"emptyLinePlaceholder":130},[81,803,805],{"class":83,"line":804},33,[81,806,131],{"emptyLinePlaceholder":130},[81,808,810,812,814,817,820,823],{"class":83,"line":809},34,[81,811,279],{"class":104},[81,813,282],{"class":104},[81,815,816],{"class":285}," get_broken_session",[81,818,819],{"class":100},"() -> AsyncGenerator[AsyncSession, ",[81,821,822],{"class":117},"None",[81,824,825],{"class":100},"]:\n",[81,827,829,832,835,838,841],{"class":83,"line":828},35,[81,830,831],{"class":104},"    async",[81,833,834],{"class":104}," with",[81,836,837],{"class":100}," BrokenSessionLocal() ",[81,839,840],{"class":104},"as",[81,842,843],{"class":100}," session:\n",[81,845,847,850],{"class":83,"line":846},36,[81,848,849],{"class":104},"        yield",[81,851,852],{"class":100}," session\n",[81,854,856,859],{"class":83,"line":855},37,[81,857,858],{"class":104},"        await",[81,860,365],{"class":100},[81,862,864],{"class":83,"line":863},38,[81,865,866],{"class":87},"        # session closes here — all attributes expired\n",[81,868,870],{"class":83,"line":869},39,[81,871,131],{"emptyLinePlaceholder":130},[81,873,875],{"class":83,"line":874},40,[81,876,131],{"emptyLinePlaceholder":130},[81,878,880,882,884,887,889,891],{"class":83,"line":879},41,[81,881,279],{"class":104},[81,883,282],{"class":104},[81,885,886],{"class":285}," get_fixed_session",[81,888,819],{"class":100},[81,890,822],{"class":117},[81,892,825],{"class":100},[81,894,896,898,900,903,905],{"class":83,"line":895},42,[81,897,831],{"class":104},[81,899,834],{"class":104},[81,901,902],{"class":100}," FixedSessionLocal() ",[81,904,840],{"class":104},[81,906,843],{"class":100},[81,908,910,912],{"class":83,"line":909},43,[81,911,849],{"class":104},[81,913,852],{"class":100},[81,915,917,919],{"class":83,"line":916},44,[81,918,858],{"class":104},[81,920,365],{"class":100},[81,922,924],{"class":83,"line":923},45,[81,925,926],{"class":87},"        # attributes remain in-memory after commit\n",[81,928,930],{"class":83,"line":929},46,[81,931,131],{"emptyLinePlaceholder":130},[81,933,935],{"class":83,"line":934},47,[81,936,131],{"emptyLinePlaceholder":130},[81,938,940,943,945,948],{"class":83,"line":939},48,[81,941,942],{"class":285},"@app.post",[81,944,577],{"class":100},[81,946,947],{"class":171},"\"\u002Fusers-broken\"",[81,949,186],{"class":100},[81,951,953,955,957,960],{"class":83,"line":952},49,[81,954,279],{"class":104},[81,956,282],{"class":104},[81,958,959],{"class":285}," create_user_broken",[81,961,962],{"class":100},"(\n",[81,964,966,969,971,974,976],{"class":83,"line":965},50,[81,967,968],{"class":100},"    name: ",[81,970,405],{"class":117},[81,972,973],{"class":100},", session: AsyncSession ",[81,975,105],{"class":104},[81,977,978],{"class":100}," Depends(get_broken_session)\n",[81,980,982,985,988],{"class":83,"line":981},51,[81,983,984],{"class":100},") -> ",[81,986,987],{"class":117},"dict",[81,989,990],{"class":100},":\n",[81,992,994,996,998,1000,1002,1004,1007,1010,1012,1015,1018,1021,1023,1026,1029],{"class":83,"line":993},52,[81,995,413],{"class":100},[81,997,105],{"class":104},[81,999,418],{"class":100},[81,1001,421],{"class":111},[81,1003,105],{"class":104},[81,1005,1006],{"class":100},"name, ",[81,1008,1009],{"class":111},"email",[81,1011,105],{"class":104},[81,1013,1014],{"class":104},"f",[81,1016,1017],{"class":171},"\"",[81,1019,1020],{"class":117},"{",[81,1022,421],{"class":100},[81,1024,1025],{"class":117},"}",[81,1027,1028],{"class":171},"@example.com\"",[81,1030,186],{"class":100},[81,1032,1034],{"class":83,"line":1033},53,[81,1035,431],{"class":100},[81,1037,1039],{"class":83,"line":1038},54,[81,1040,1041],{"class":87},"    # commit happens in dependency teardown, AFTER this function returns\n",[81,1043,1045],{"class":83,"line":1044},55,[81,1046,1047],{"class":87},"    # Pydantic tries to access user.id AFTER the session is closed\n",[81,1049,1051,1053,1056,1059,1062,1065,1068],{"class":83,"line":1050},56,[81,1052,377],{"class":104},[81,1054,1055],{"class":100}," {",[81,1057,1058],{"class":171},"\"id\"",[81,1060,1061],{"class":100},": user.id, ",[81,1063,1064],{"class":171},"\"name\"",[81,1066,1067],{"class":100},": user.name}  ",[81,1069,1070],{"class":87},"# DetachedInstanceError here\n",[81,1072,1074],{"class":83,"line":1073},57,[81,1075,131],{"emptyLinePlaceholder":130},[81,1077,1079],{"class":83,"line":1078},58,[81,1080,131],{"emptyLinePlaceholder":130},[81,1082,1084,1086,1088,1091],{"class":83,"line":1083},59,[81,1085,942],{"class":285},[81,1087,577],{"class":100},[81,1089,1090],{"class":171},"\"\u002Fusers-fixed\"",[81,1092,186],{"class":100},[81,1094,1096,1098,1100,1103],{"class":83,"line":1095},60,[81,1097,279],{"class":104},[81,1099,282],{"class":104},[81,1101,1102],{"class":285}," create_user_fixed",[81,1104,962],{"class":100},[81,1106,1108,1110,1112,1114,1116],{"class":83,"line":1107},61,[81,1109,968],{"class":100},[81,1111,405],{"class":117},[81,1113,973],{"class":100},[81,1115,105],{"class":104},[81,1117,1118],{"class":100}," Depends(get_fixed_session)\n",[81,1120,1122,1124,1126],{"class":83,"line":1121},62,[81,1123,984],{"class":100},[81,1125,987],{"class":117},[81,1127,990],{"class":100},[81,1129,1131,1133,1135,1137,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159],{"class":83,"line":1130},63,[81,1132,413],{"class":100},[81,1134,105],{"class":104},[81,1136,418],{"class":100},[81,1138,421],{"class":111},[81,1140,105],{"class":104},[81,1142,1006],{"class":100},[81,1144,1009],{"class":111},[81,1146,105],{"class":104},[81,1148,1014],{"class":104},[81,1150,1017],{"class":171},[81,1152,1020],{"class":117},[81,1154,421],{"class":100},[81,1156,1025],{"class":117},[81,1158,1028],{"class":171},[81,1160,186],{"class":100},[81,1162,1164],{"class":83,"line":1163},64,[81,1165,431],{"class":100},[81,1167,1169],{"class":83,"line":1168},65,[81,1170,1171],{"class":87},"    # expire_on_commit=False keeps attributes loaded after dependency teardown\n",[81,1173,1175,1177,1179,1181,1183,1185,1187],{"class":83,"line":1174},66,[81,1176,377],{"class":104},[81,1178,1055],{"class":100},[81,1180,1058],{"class":171},[81,1182,1061],{"class":100},[81,1184,1064],{"class":171},[81,1186,1067],{"class":100},[81,1188,1189],{"class":87},"# Safe\n",[14,1191,1192],{},"The root cause sequence is:",[1194,1195,1196,1203,1213,1223],"ol",{},[1197,1198,1199,1202],"li",{},[18,1200,1201],{},"session.commit()"," is called — SQLAlchemy expires all attributes on tracked objects.",[1197,1204,1205,1208,1209,1212],{},[18,1206,1207],{},"session.close()"," is called (or the ",[18,1210,1211],{},"async with"," block exits) — the session is no longer usable.",[1197,1214,1215,1216,1219,1220,1222],{},"Application code accesses ",[18,1217,1218],{},"user.id"," or another attribute — SQLAlchemy tries to lazy-load it by issuing a ",[18,1221,63],{},", but there is no open session to execute through.",[1197,1224,1225,1227],{},[18,1226,32],{}," is raised.",[14,1229,1230],{},"This exact sequence also occurs when objects are passed across task boundaries in Celery or asyncio task queues — the session that created the object closes in one task, and the consuming task attempts attribute access in another execution context.",[45,1232,1234],{"id":1233},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1236,1237,1238,1254],"table",{},[1239,1240,1241],"thead",{},[1242,1243,1244,1248,1251],"tr",{},[1245,1246,1247],"th",{},"Error \u002F Warning",[1245,1249,1250],{},"Root Cause",[1245,1252,1253],{},"Production Fix",[1255,1256,1257,1288,1310,1325,1364,1383],"tbody",{},[1242,1258,1259,1265,1273],{},[1260,1261,1262],"td",{},[18,1263,1264],{},"sqlalchemy.orm.exc.DetachedInstanceError: Instance \u003CUser at 0x...> is not bound to a Session; attribute refresh operation cannot proceed",[1260,1266,1267,1269,1270,1272],{},[18,1268,59],{}," (default) expires all attributes on ",[18,1271,36],{},". Attribute accessed after session closes.",[1260,1274,16,1275,1277,1278,1280,1281,1283,1284,1287],{},[18,1276,20],{}," on ",[18,1279,24],{},"\u002F",[18,1282,28],{},", or call ",[18,1285,1286],{},"session.refresh(obj)"," before session closes.",[1242,1289,1290,1297,1300],{},[1260,1291,1292,1294,1295],{},[18,1293,32],{}," on relationship access after ",[18,1296,36],{},[1260,1298,1299],{},"Relationship was not loaded before commit; lazy loading attempted on detached object.",[1260,1301,1302,1303,25,1306,1309],{},"Use ",[18,1304,1305],{},"selectinload()",[18,1307,1308],{},"joinedload()"," in the query to eagerly load required relationships before committing.",[1242,1311,1312,1317,1320],{},[1260,1313,1314,1316],{},[18,1315,32],{}," when passing ORM objects to Celery tasks",[1260,1318,1319],{},"Session closed in the producing context; Celery worker accesses attributes in a different process\u002Fthread.",[1260,1321,1302,1322,1324],{},[18,1323,20],{}," and serialize to dict\u002FPydantic schema before enqueuing. Never pass live ORM instances across process boundaries.",[1242,1326,1327,1341,1355],{},[1260,1328,1329,1277,1331,1334,1335,1338,1339],{},[18,1330,32],{},[18,1332,1333],{},"obj.id"," immediately after ",[18,1336,1337],{},"session.add()"," then ",[18,1340,1201],{},[1260,1342,1343,1346,1347,1349,1350,175,1352,1354],{},[18,1344,1345],{},"id"," is a server-generated primary key (autoincrement \u002F sequence). After ",[18,1348,36],{}," with ",[18,1351,59],{},[18,1353,1345],{}," is expired — even though it was loaded during flush.",[1260,1356,1357,1358,1361,1362,67],{},"Call ",[18,1359,1360],{},"await session.refresh(user)"," after commit, or set ",[18,1363,20],{},[1242,1365,1366,1371,1377],{},[1260,1367,1368],{},[18,1369,1370],{},"sqlalchemy.exc.InvalidRequestError: Instance \u003CUser> is not persistent within this Session",[1260,1372,1373,1374,1376],{},"Attempting ",[18,1375,1286],{}," on an object that is already detached from this session (different session instance).",[1260,1378,1302,1379,1382],{},[18,1380,1381],{},"session.merge(obj)"," to re-integrate the object, then refresh if needed.",[1242,1384,1385,1390,1397],{},[1260,1386,1387],{},[18,1388,1389],{},"MissingGreenlet: greenlet_spawn has not been called",[1260,1391,1392,1393,1396],{},"Accessing a lazy-loaded relationship inside an async context without ",[18,1394,1395],{},"await",", causing SQLAlchemy to attempt sync I\u002FO.",[1260,1398,1399,1400,1402,1403,1406,1407,67],{},"Eager-load with ",[18,1401,1305],{}," or use ",[18,1404,1405],{},"AsyncSession.run_sync()"," for explicit sync bridging. See the ",[39,1408,1410],{"href":1409},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Ffixing-greenletspawnerror-in-async-sqlalchemy-workflows\u002F","GreenletSpawnError guide",[1412,1413,1415],"h3",{"id":1414},"diagnosing-which-attributes-are-expired","Diagnosing Which Attributes Are Expired",[14,1417,1302,1418,1421],{},[18,1419,1420],{},"inspect()"," to see exactly which attributes are expired on a given object before you hit the error in production:",[72,1423,1425],{"className":74,"code":1424,"language":76,"meta":77,"style":77},"from __future__ import annotations\n\nfrom typing import Any\nfrom sqlalchemy import inspect as sa_inspect\nfrom sqlalchemy.orm import InstanceState\n\n\ndef list_expired_attrs(obj: Any) -> list[str]:\n    \"\"\"Return attribute names that will trigger a lazy SELECT on next access.\"\"\"\n    state: InstanceState = sa_inspect(obj)\n    return list(state.expired_attributes)\n\n\n# Usage: call before session.close() to audit what will be unavailable\nasync def debug_expiry(session: AsyncSession, user_id: int) -> None:\n    result = await session.execute(select(User).where(User.id == user_id))\n    user = result.scalar_one()\n    await session.commit()  # expire_on_commit=True causes expiry here\n\n    expired = list_expired_attrs(user)\n    print(f\"Expired attrs after commit: {expired}\")\n    # Typical output: ['name', 'email', 'created_at', ...]\n    # All will raise DetachedInstanceError if session is now closed\n",[18,1426,1427,1437,1441,1452,1468,1479,1483,1487,1502,1507,1517,1527,1531,1535,1540,1560,1576,1584,1594,1598,1608,1631,1636],{"__ignoreMap":77},[81,1428,1429,1431,1433,1435],{"class":83,"line":84},[81,1430,143],{"class":104},[81,1432,478],{"class":117},[81,1434,481],{"class":104},[81,1436,484],{"class":100},[81,1438,1439],{"class":83,"line":91},[81,1440,131],{"emptyLinePlaceholder":130},[81,1442,1443,1445,1447,1449],{"class":83,"line":97},[81,1444,143],{"class":104},[81,1446,495],{"class":100},[81,1448,149],{"class":104},[81,1450,1451],{"class":100}," Any\n",[81,1453,1454,1456,1458,1460,1463,1465],{"class":83,"line":127},[81,1455,143],{"class":104},[81,1457,253],{"class":100},[81,1459,149],{"class":104},[81,1461,1462],{"class":100}," inspect ",[81,1464,840],{"class":104},[81,1466,1467],{"class":100}," sa_inspect\n",[81,1469,1470,1472,1474,1476],{"class":83,"line":134},[81,1471,143],{"class":104},[81,1473,265],{"class":100},[81,1475,149],{"class":104},[81,1477,1478],{"class":100}," InstanceState\n",[81,1480,1481],{"class":83,"line":140},[81,1482,131],{"emptyLinePlaceholder":130},[81,1484,1485],{"class":83,"line":155},[81,1486,131],{"emptyLinePlaceholder":130},[81,1488,1489,1492,1495,1498,1500],{"class":83,"line":160},[81,1490,1491],{"class":104},"def",[81,1493,1494],{"class":285}," list_expired_attrs",[81,1496,1497],{"class":100},"(obj: Any) -> list[",[81,1499,405],{"class":117},[81,1501,825],{"class":100},[81,1503,1504],{"class":83,"line":189},[81,1505,1506],{"class":171},"    \"\"\"Return attribute names that will trigger a lazy SELECT on next access.\"\"\"\n",[81,1508,1509,1512,1514],{"class":83,"line":200},[81,1510,1511],{"class":100},"    state: InstanceState ",[81,1513,105],{"class":104},[81,1515,1516],{"class":100}," sa_inspect(obj)\n",[81,1518,1519,1521,1524],{"class":83,"line":206},[81,1520,377],{"class":104},[81,1522,1523],{"class":117}," list",[81,1525,1526],{"class":100},"(state.expired_attributes)\n",[81,1528,1529],{"class":83,"line":217},[81,1530,131],{"emptyLinePlaceholder":130},[81,1532,1533],{"class":83,"line":234},[81,1534,131],{"emptyLinePlaceholder":130},[81,1536,1537],{"class":83,"line":368},[81,1538,1539],{"class":87},"# Usage: call before session.close() to audit what will be unavailable\n",[81,1541,1542,1544,1546,1549,1552,1554,1556,1558],{"class":83,"line":374},[81,1543,279],{"class":104},[81,1545,282],{"class":104},[81,1547,1548],{"class":285}," debug_expiry",[81,1550,1551],{"class":100},"(session: AsyncSession, user_id: ",[81,1553,292],{"class":117},[81,1555,984],{"class":100},[81,1557,822],{"class":117},[81,1559,990],{"class":100},[81,1561,1562,1564,1566,1568,1571,1573],{"class":83,"line":598},[81,1563,339],{"class":100},[81,1565,105],{"class":104},[81,1567,344],{"class":104},[81,1569,1570],{"class":100}," session.execute(select(User).where(User.id ",[81,1572,318],{"class":104},[81,1574,1575],{"class":100}," user_id))\n",[81,1577,1578,1580,1582],{"class":83,"line":613},[81,1579,413],{"class":100},[81,1581,105],{"class":104},[81,1583,357],{"class":100},[81,1585,1586,1588,1591],{"class":83,"line":624},[81,1587,362],{"class":104},[81,1589,1590],{"class":100}," session.commit()  ",[81,1592,1593],{"class":87},"# expire_on_commit=True causes expiry here\n",[81,1595,1596],{"class":83,"line":652},[81,1597,131],{"emptyLinePlaceholder":130},[81,1599,1600,1603,1605],{"class":83,"line":673},[81,1601,1602],{"class":100},"    expired ",[81,1604,105],{"class":104},[81,1606,1607],{"class":100}," list_expired_attrs(user)\n",[81,1609,1610,1613,1615,1617,1620,1622,1625,1627,1629],{"class":83,"line":692},[81,1611,1612],{"class":117},"    print",[81,1614,577],{"class":100},[81,1616,1014],{"class":104},[81,1618,1619],{"class":171},"\"Expired attrs after commit: ",[81,1621,1020],{"class":117},[81,1623,1624],{"class":100},"expired",[81,1626,1025],{"class":117},[81,1628,1017],{"class":171},[81,1630,186],{"class":100},[81,1632,1633],{"class":83,"line":697},[81,1634,1635],{"class":87},"    # Typical output: ['name', 'email', 'created_at', ...]\n",[81,1637,1638],{"class":83,"line":702},[81,1639,1640],{"class":87},"    # All will raise DetachedInstanceError if session is now closed\n",[45,1642,1644],{"id":1643},"advanced-session-expiry-optimization","Advanced Session Expiry Optimization",[1412,1646,1648,1649],{"id":1647},"selective-per-object-expiry-with-sessionexpireobj-attrs","Selective Per-Object expiry with ",[18,1650,1651],{},"session.expire(obj, attrs)",[14,1653,1654,1655,1657],{},"When ",[18,1656,20],{}," is set globally but you know a specific object might be stale (for example, it was modified by a background job between your last read and the current request), you can expire only the attributes that matter:",[72,1659,1661],{"className":74,"code":1660,"language":76,"meta":77,"style":77},"from __future__ import annotations\n\nfrom sqlalchemy import Integer, String, Numeric\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom decimal import Decimal\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Product(Base):\n    __tablename__ = \"products\"\n    id: Mapped[int] = mapped_column(Integer, primary_key=True)\n    name: Mapped[str] = mapped_column(String(200))\n    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))\n    stock_count: Mapped[int] = mapped_column(Integer)\n\n\nasync def get_fresh_price(session: AsyncSession, product_id: int) -> Decimal:\n    product = await session.get(Product, product_id)\n    if product is None:\n        raise ValueError(f\"Product {product_id} not found\")\n\n    # Only expire the price column — force re-read from DB without full refresh\n    session.expire(product, [\"price\"])\n    # Next access to product.price triggers a targeted SELECT\n    return product.price  # Safe — triggers a single-column reload\n",[18,1662,1663,1673,1677,1688,1698,1709,1721,1725,1729,1741,1745,1749,1753,1766,1775,1797,1814,1833,1847,1851,1855,1872,1884,1900,1927,1931,1936,1947,1952],{"__ignoreMap":77},[81,1664,1665,1667,1669,1671],{"class":83,"line":84},[81,1666,143],{"class":104},[81,1668,478],{"class":117},[81,1670,481],{"class":104},[81,1672,484],{"class":100},[81,1674,1675],{"class":83,"line":91},[81,1676,131],{"emptyLinePlaceholder":130},[81,1678,1679,1681,1683,1685],{"class":83,"line":97},[81,1680,143],{"class":104},[81,1682,253],{"class":100},[81,1684,149],{"class":104},[81,1686,1687],{"class":100}," Integer, String, Numeric\n",[81,1689,1690,1692,1694,1696],{"class":83,"line":127},[81,1691,143],{"class":104},[81,1693,265],{"class":100},[81,1695,149],{"class":104},[81,1697,534],{"class":100},[81,1699,1700,1702,1704,1706],{"class":83,"line":134},[81,1701,143],{"class":104},[81,1703,146],{"class":100},[81,1705,149],{"class":104},[81,1707,1708],{"class":100}," AsyncSession\n",[81,1710,1711,1713,1716,1718],{"class":83,"line":140},[81,1712,143],{"class":104},[81,1714,1715],{"class":100}," decimal ",[81,1717,149],{"class":104},[81,1719,1720],{"class":100}," Decimal\n",[81,1722,1723],{"class":83,"line":155},[81,1724,131],{"emptyLinePlaceholder":130},[81,1726,1727],{"class":83,"line":160},[81,1728,131],{"emptyLinePlaceholder":130},[81,1730,1731,1733,1735,1737,1739],{"class":83,"line":189},[81,1732,571],{"class":104},[81,1734,574],{"class":285},[81,1736,577],{"class":100},[81,1738,580],{"class":285},[81,1740,295],{"class":100},[81,1742,1743],{"class":83,"line":200},[81,1744,587],{"class":104},[81,1746,1747],{"class":83,"line":206},[81,1748,131],{"emptyLinePlaceholder":130},[81,1750,1751],{"class":83,"line":217},[81,1752,131],{"emptyLinePlaceholder":130},[81,1754,1755,1757,1760,1762,1764],{"class":83,"line":234},[81,1756,571],{"class":104},[81,1758,1759],{"class":285}," Product",[81,1761,577],{"class":100},[81,1763,608],{"class":285},[81,1765,295],{"class":100},[81,1767,1768,1770,1772],{"class":83,"line":368},[81,1769,616],{"class":100},[81,1771,105],{"class":104},[81,1773,1774],{"class":171}," \"products\"\n",[81,1776,1777,1779,1781,1783,1785,1787,1789,1791,1793,1795],{"class":83,"line":374},[81,1778,627],{"class":117},[81,1780,630],{"class":100},[81,1782,292],{"class":117},[81,1784,635],{"class":100},[81,1786,105],{"class":104},[81,1788,640],{"class":100},[81,1790,643],{"class":111},[81,1792,105],{"class":104},[81,1794,118],{"class":117},[81,1796,186],{"class":100},[81,1798,1799,1801,1803,1805,1807,1809,1812],{"class":83,"line":598},[81,1800,655],{"class":100},[81,1802,405],{"class":117},[81,1804,635],{"class":100},[81,1806,105],{"class":104},[81,1808,664],{"class":100},[81,1810,1811],{"class":117},"200",[81,1813,670],{"class":100},[81,1815,1816,1819,1821,1824,1826,1828,1831],{"class":83,"line":613},[81,1817,1818],{"class":100},"    price: Mapped[Decimal] ",[81,1820,105],{"class":104},[81,1822,1823],{"class":100}," mapped_column(Numeric(",[81,1825,183],{"class":117},[81,1827,175],{"class":100},[81,1829,1830],{"class":117},"2",[81,1832,670],{"class":100},[81,1834,1835,1838,1840,1842,1844],{"class":83,"line":624},[81,1836,1837],{"class":100},"    stock_count: Mapped[",[81,1839,292],{"class":117},[81,1841,635],{"class":100},[81,1843,105],{"class":104},[81,1845,1846],{"class":100}," mapped_column(Integer)\n",[81,1848,1849],{"class":83,"line":652},[81,1850,131],{"emptyLinePlaceholder":130},[81,1852,1853],{"class":83,"line":673},[81,1854,131],{"emptyLinePlaceholder":130},[81,1856,1857,1859,1861,1864,1867,1869],{"class":83,"line":692},[81,1858,279],{"class":104},[81,1860,282],{"class":104},[81,1862,1863],{"class":285}," get_fresh_price",[81,1865,1866],{"class":100},"(session: AsyncSession, product_id: ",[81,1868,292],{"class":117},[81,1870,1871],{"class":100},") -> Decimal:\n",[81,1873,1874,1877,1879,1881],{"class":83,"line":697},[81,1875,1876],{"class":100},"    product ",[81,1878,105],{"class":104},[81,1880,344],{"class":104},[81,1882,1883],{"class":100}," session.get(Product, product_id)\n",[81,1885,1886,1889,1892,1895,1898],{"class":83,"line":702},[81,1887,1888],{"class":104},"    if",[81,1890,1891],{"class":100}," product ",[81,1893,1894],{"class":104},"is",[81,1896,1897],{"class":117}," None",[81,1899,990],{"class":100},[81,1901,1902,1905,1908,1910,1912,1915,1917,1920,1922,1925],{"class":83,"line":723},[81,1903,1904],{"class":104},"        raise",[81,1906,1907],{"class":117}," ValueError",[81,1909,577],{"class":100},[81,1911,1014],{"class":104},[81,1913,1914],{"class":171},"\"Product ",[81,1916,1020],{"class":117},[81,1918,1919],{"class":100},"product_id",[81,1921,1025],{"class":117},[81,1923,1924],{"class":171}," not found\"",[81,1926,186],{"class":100},[81,1928,1929],{"class":83,"line":728},[81,1930,131],{"emptyLinePlaceholder":130},[81,1932,1933],{"class":83,"line":734},[81,1934,1935],{"class":87},"    # Only expire the price column — force re-read from DB without full refresh\n",[81,1937,1938,1941,1944],{"class":83,"line":753},[81,1939,1940],{"class":100},"    session.expire(product, [",[81,1942,1943],{"class":171},"\"price\"",[81,1945,1946],{"class":100},"])\n",[81,1948,1949],{"class":83,"line":758},[81,1950,1951],{"class":87},"    # Next access to product.price triggers a targeted SELECT\n",[81,1953,1954,1956,1959],{"class":83,"line":764},[81,1955,377],{"class":104},[81,1957,1958],{"class":100}," product.price  ",[81,1960,1961],{"class":87},"# Safe — triggers a single-column reload\n",[14,1963,1964,1965,1967],{},"This technique avoids the overhead of ",[18,1966,1286],{}," (which re-fetches all columns) when only one or two columns are expected to change externally.",[1412,1969,1971,1972,1975],{"id":1970},"using-sessionmerge-for-reintegration-after-session-close","Using ",[18,1973,1974],{},"session.merge()"," for Reintegration After Session Close",[14,1977,1978,1979,1981,1982,1984],{},"When an object was loaded in one request context and must be used in another (e.g., cached in-memory between API calls), ",[18,1980,1974],{}," is safer than ",[18,1983,1337],{},":",[72,1986,1988],{"className":74,"code":1987,"language":76,"meta":77,"style":77},"from __future__ import annotations\n\nfrom sqlalchemy import Integer, String\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Tenant(Base):\n    __tablename__ = \"tenants\"\n    id: Mapped[int] = mapped_column(Integer, primary_key=True)\n    name: Mapped[str] = mapped_column(String(200))\n    plan: Mapped[str] = mapped_column(String(50))\n\n\n# Simulating a detached object retrieved from application cache\ncached_tenant = Tenant(id=7, name=\"Acme Corp\", plan=\"enterprise\")\n\nasync def update_tenant_plan(session: AsyncSession, cached_obj: Tenant, new_plan: str) -> Tenant:\n    # merge() loads current DB state, overlays cached_obj's attributes, returns persistent copy\n    merged = await session.merge(cached_obj)\n    merged.plan = new_plan\n    await session.commit()\n    # merged is persistent; with expire_on_commit=False its attributes are still valid\n    return merged\n",[18,1989,1990,2000,2004,2015,2025,2035,2039,2043,2055,2059,2063,2067,2080,2089,2111,2127,2145,2149,2153,2158,2196,2200,2217,2222,2234,2244,2250,2255],{"__ignoreMap":77},[81,1991,1992,1994,1996,1998],{"class":83,"line":84},[81,1993,143],{"class":104},[81,1995,478],{"class":117},[81,1997,481],{"class":104},[81,1999,484],{"class":100},[81,2001,2002],{"class":83,"line":91},[81,2003,131],{"emptyLinePlaceholder":130},[81,2005,2006,2008,2010,2012],{"class":83,"line":97},[81,2007,143],{"class":104},[81,2009,253],{"class":100},[81,2011,149],{"class":104},[81,2013,2014],{"class":100}," Integer, String\n",[81,2016,2017,2019,2021,2023],{"class":83,"line":127},[81,2018,143],{"class":104},[81,2020,265],{"class":100},[81,2022,149],{"class":104},[81,2024,534],{"class":100},[81,2026,2027,2029,2031,2033],{"class":83,"line":134},[81,2028,143],{"class":104},[81,2030,146],{"class":100},[81,2032,149],{"class":104},[81,2034,1708],{"class":100},[81,2036,2037],{"class":83,"line":140},[81,2038,131],{"emptyLinePlaceholder":130},[81,2040,2041],{"class":83,"line":155},[81,2042,131],{"emptyLinePlaceholder":130},[81,2044,2045,2047,2049,2051,2053],{"class":83,"line":160},[81,2046,571],{"class":104},[81,2048,574],{"class":285},[81,2050,577],{"class":100},[81,2052,580],{"class":285},[81,2054,295],{"class":100},[81,2056,2057],{"class":83,"line":189},[81,2058,587],{"class":104},[81,2060,2061],{"class":83,"line":200},[81,2062,131],{"emptyLinePlaceholder":130},[81,2064,2065],{"class":83,"line":206},[81,2066,131],{"emptyLinePlaceholder":130},[81,2068,2069,2071,2074,2076,2078],{"class":83,"line":217},[81,2070,571],{"class":104},[81,2072,2073],{"class":285}," Tenant",[81,2075,577],{"class":100},[81,2077,608],{"class":285},[81,2079,295],{"class":100},[81,2081,2082,2084,2086],{"class":83,"line":234},[81,2083,616],{"class":100},[81,2085,105],{"class":104},[81,2087,2088],{"class":171}," \"tenants\"\n",[81,2090,2091,2093,2095,2097,2099,2101,2103,2105,2107,2109],{"class":83,"line":368},[81,2092,627],{"class":117},[81,2094,630],{"class":100},[81,2096,292],{"class":117},[81,2098,635],{"class":100},[81,2100,105],{"class":104},[81,2102,640],{"class":100},[81,2104,643],{"class":111},[81,2106,105],{"class":104},[81,2108,118],{"class":117},[81,2110,186],{"class":100},[81,2112,2113,2115,2117,2119,2121,2123,2125],{"class":83,"line":374},[81,2114,655],{"class":100},[81,2116,405],{"class":117},[81,2118,635],{"class":100},[81,2120,105],{"class":104},[81,2122,664],{"class":100},[81,2124,1811],{"class":117},[81,2126,670],{"class":100},[81,2128,2129,2132,2134,2136,2138,2140,2143],{"class":83,"line":598},[81,2130,2131],{"class":100},"    plan: Mapped[",[81,2133,405],{"class":117},[81,2135,635],{"class":100},[81,2137,105],{"class":104},[81,2139,664],{"class":100},[81,2141,2142],{"class":117},"50",[81,2144,670],{"class":100},[81,2146,2147],{"class":83,"line":613},[81,2148,131],{"emptyLinePlaceholder":130},[81,2150,2151],{"class":83,"line":624},[81,2152,131],{"emptyLinePlaceholder":130},[81,2154,2155],{"class":83,"line":652},[81,2156,2157],{"class":87},"# Simulating a detached object retrieved from application cache\n",[81,2159,2160,2163,2165,2168,2170,2172,2175,2177,2179,2181,2184,2186,2189,2191,2194],{"class":83,"line":673},[81,2161,2162],{"class":100},"cached_tenant ",[81,2164,105],{"class":104},[81,2166,2167],{"class":100}," Tenant(",[81,2169,1345],{"class":111},[81,2171,105],{"class":104},[81,2173,2174],{"class":117},"7",[81,2176,175],{"class":100},[81,2178,421],{"class":111},[81,2180,105],{"class":104},[81,2182,2183],{"class":171},"\"Acme Corp\"",[81,2185,175],{"class":100},[81,2187,2188],{"class":111},"plan",[81,2190,105],{"class":104},[81,2192,2193],{"class":171},"\"enterprise\"",[81,2195,186],{"class":100},[81,2197,2198],{"class":83,"line":692},[81,2199,131],{"emptyLinePlaceholder":130},[81,2201,2202,2204,2206,2209,2212,2214],{"class":83,"line":697},[81,2203,279],{"class":104},[81,2205,282],{"class":104},[81,2207,2208],{"class":285}," update_tenant_plan",[81,2210,2211],{"class":100},"(session: AsyncSession, cached_obj: Tenant, new_plan: ",[81,2213,405],{"class":117},[81,2215,2216],{"class":100},") -> Tenant:\n",[81,2218,2219],{"class":83,"line":702},[81,2220,2221],{"class":87},"    # merge() loads current DB state, overlays cached_obj's attributes, returns persistent copy\n",[81,2223,2224,2227,2229,2231],{"class":83,"line":723},[81,2225,2226],{"class":100},"    merged ",[81,2228,105],{"class":104},[81,2230,344],{"class":104},[81,2232,2233],{"class":100}," session.merge(cached_obj)\n",[81,2235,2236,2239,2241],{"class":83,"line":728},[81,2237,2238],{"class":100},"    merged.plan ",[81,2240,105],{"class":104},[81,2242,2243],{"class":100}," new_plan\n",[81,2245,2246,2248],{"class":83,"line":734},[81,2247,362],{"class":104},[81,2249,365],{"class":100},[81,2251,2252],{"class":83,"line":753},[81,2253,2254],{"class":87},"    # merged is persistent; with expire_on_commit=False its attributes are still valid\n",[81,2256,2257,2259],{"class":83,"line":758},[81,2258,377],{"class":104},[81,2260,2261],{"class":100}," merged\n",[14,2263,2264,2266,2267,2269,2270,2273,2274,2277],{},[18,2265,1974],{}," issues a ",[18,2268,63],{}," by primary key if the object is not already in the identity map, then merges the provided attribute values onto the loaded instance. The original ",[18,2271,2272],{},"cached_obj"," remains detached and unchanged — only the returned ",[18,2275,2276],{},"merged"," object is managed by the session.",[45,2279,2281],{"id":2280},"frequently-asked-questions","Frequently Asked Questions",[14,2283,2284,2291,2292,2294,2295,2297],{},[2285,2286,2287,2288,2290],"strong",{},"Is it safe to set ",[18,2289,20],{}," globally?","\nYes, for the vast majority of web API workloads where each request gets its own session. Attributes loaded during the request remain valid for the duration of response serialization. The risk is staleness: if another process modifies the same row between your ",[18,2293,36],{}," and your next read in a long-lived session, you may serve outdated data. For long-lived sessions or background workers processing the same rows repeatedly, use ",[18,2296,1286],{}," at the start of each processing cycle rather than relying on post-commit expiry.",[14,2299,2300,2311,2312,2314,2315,2317,2318,2320,2321,2323,2324,2326,2327,1334,2329,67],{},[2285,2301,2302,2303,2305,2306,53,2308,2310],{},"Why does ",[18,2304,1218],{}," raise ",[18,2307,32],{},[18,2309,36],{}," when the id was clearly set during insert?","\nSQLAlchemy's ",[18,2313,59],{}," (the default) expires every attribute on every tracked object after ",[18,2316,36],{}," — including ",[18,2319,1345],{},". Even though the database assigned the id during the flush, SQLAlchemy marks it as expired so the next access re-reads from the database. If the session closes before that access, the re-read is impossible and ",[18,2322,32],{}," is raised. Fix: ",[18,2325,20],{},", or ",[18,2328,1360],{},[18,2330,2331],{},"await session.commit()",[14,2333,2334,2344,2346,2347,2349,2350,2352,2353,2355,2356,2359,2360,2363],{},[2285,2335,2336,2337,2339,2340,2343],{},"What is the difference between ",[18,2338,1286],{}," and ",[18,2341,2342],{},"session.expire(obj)","?",[18,2345,2342],{}," marks the object's attributes as expired without issuing any SQL. The next attribute access triggers a lazy ",[18,2348,63],{}," to reload them from the database. ",[18,2351,1286],{}," immediately issues a ",[18,2354,63],{}," and reloads all columns (or a specified subset) right now, synchronously within the current async context. Use ",[18,2357,2358],{},"expire()"," when you want deferred reloading on demand; use ",[18,2361,2362],{},"refresh()"," when you need the fresh value immediately.",[14,2365,2366,2374,2375,2377,2378,25,2381,2383,2384,2386],{},[2285,2367,2368,2369,2371,2372,2343],{},"Can ",[18,2370,32],{}," occur even with ",[18,2373,20],{},"\nYes — ",[18,2376,20],{}," only prevents expiry at commit time. The error can still occur if you explicitly call ",[18,2379,2380],{},"session.expunge(obj)",[18,2382,1207],{}," and then access unloaded attributes. It also occurs if you access lazy-loaded relationships that were never fetched, regardless of expiry settings. The fix for those cases is eager loading with ",[18,2385,1305],{}," before detachment.",[45,2388,2390],{"id":2389},"related","Related",[2392,2393,2394,2402,2416,2423],"ul",{},[1197,2395,2396,2398,2399,2401],{},[39,2397,42],{"href":41}," — Parent guide covering all five object states, identity map semantics, and ",[18,2400,112],{}," configuration in depth.",[1197,2403,2404,2408,2409,2339,2412,2415],{},[39,2405,2407],{"href":2406},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002Funderstanding-sessionexpunge-vs-sessionclear-in-python\u002F","Understanding Session.expunge vs Session.clear in Python"," — Sibling guide on deliberate detachment via ",[18,2410,2411],{},"expunge()",[18,2413,2414],{},"clear()",", including safe pre-load patterns.",[1197,2417,2418,2422],{},[39,2419,2421],{"href":2420},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies\u002F","Using expire_on_commit=False in FastAPI Dependencies"," — Framework-specific implementation of the recommended fix with full FastAPI dependency code.",[1197,2424,2425,2428],{},[39,2426,2427],{"href":1409},"Fixing GreenletSpawnError in Async SQLAlchemy Workflows"," — Related error that occurs when lazy loading is attempted inside async contexts without proper await handling.",[2430,2431,2432],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":77,"searchDepth":91,"depth":91,"links":2434},[2435,2436,2437,2440,2446,2447],{"id":47,"depth":91,"text":48},{"id":462,"depth":91,"text":463},{"id":1233,"depth":91,"text":1234,"children":2438},[2439],{"id":1414,"depth":97,"text":1415},{"id":1643,"depth":91,"text":1644,"children":2441},[2442,2444],{"id":1647,"depth":97,"text":2443},"Selective Per-Object expiry with session.expire(obj, attrs)",{"id":1970,"depth":97,"text":2445},"Using session.merge() for Reintegration After Session Close",{"id":2280,"depth":91,"text":2281},{"id":2389,"depth":91,"text":2390},"Set expire_on_commit=False on your sessionmaker or async_sessionmaker factory — this is the fastest fix for DetachedInstanceError on attribute access after commit(), and it is covered in depth in the Session Lifecycle and Scope Management guide alongside all other object state transitions.","md",{"date":2451},"2026-06-18","\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002Ffixing-detachedinstanceerror-after-commit-in-sqlalchemy",{"title":5,"description":2448},"mastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002Ffixing-detachedinstanceerror-after-commit-in-sqlalchemy\u002Findex","1RClPETo0IJFb70X_gaYdHfqB_mGFpEwnNx_ofroPR0",1781810028985]