[{"data":1,"prerenderedAt":2657},["ShallowReactive",2],{"page-\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002Fsetting-transaction-isolation-level-per-session\u002F":3},{"id":4,"title":5,"body":6,"description":2649,"extension":2650,"meta":2651,"navigation":112,"path":2653,"seo":2654,"stem":2655,"__hash__":2656},"content\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002Fsetting-transaction-isolation-level-per-session\u002Findex.md","Setting Transaction Isolation Level Per Session in SQLAlchemy 2.0",{"type":7,"value":8,"toc":2630},"minimark",[9,13,36,41,141,366,381,385,390,408,415,419,538,541,545,552,774,956,960,963,1163,1170,1174,1181,1270,1284,1288,1295,1471,1474,1478,1675,1679,1683,1696,1854,1861,1865,1876,2217,2220,2224,2227,2509,2515,2519,2547,2567,2573,2591,2595,2626],[10,11,5],"h1",{"id":12},"setting-transaction-isolation-level-per-session-in-sqlalchemy-20",[14,15,16,17,21,22,25,26,29,30,35],"p",{},"Call ",[18,19,20],"code",{},"connection.execution_options(isolation_level=\"SERIALIZABLE\")"," before the first statement in a transaction, or pass ",[18,23,24],{},"execution_options={\"isolation_level\": ...}"," to ",[18,27,28],{},"session.connection()"," — this guide, part of the ",[31,32,34],"a",{"href":33},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002F","transaction isolation and commit strategies"," reference, shows you exactly where each hook belongs and what breaks when you place it in the wrong order.",[37,38,40],"h2",{"id":39},"quick-answer","Quick Answer",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-python shiki shiki-themes github-light github-dark","# Before (SQLAlchemy 1.4 — engine-level global, no per-transaction override)\nengine = create_engine(\n    \"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\",\n    isolation_level=\"REPEATABLE_READ\",  # applies to every connection, forever\n)\n\nwith engine.connect() as conn:\n    conn.execute(text(\"SELECT ...\"))\n","python","",[18,49,50,59,73,83,101,107,114,129],{"__ignoreMap":47},[51,52,55],"span",{"class":53,"line":54},"line",1,[51,56,58],{"class":57},"sJ8bj","# Before (SQLAlchemy 1.4 — engine-level global, no per-transaction override)\n",[51,60,62,66,70],{"class":53,"line":61},2,[51,63,65],{"class":64},"sVt8B","engine ",[51,67,69],{"class":68},"szBVR","=",[51,71,72],{"class":64}," create_engine(\n",[51,74,76,80],{"class":53,"line":75},3,[51,77,79],{"class":78},"sZZnC","    \"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\"",[51,81,82],{"class":64},",\n",[51,84,86,90,92,95,98],{"class":53,"line":85},4,[51,87,89],{"class":88},"s4XuR","    isolation_level",[51,91,69],{"class":68},[51,93,94],{"class":78},"\"REPEATABLE_READ\"",[51,96,97],{"class":64},",  ",[51,99,100],{"class":57},"# applies to every connection, forever\n",[51,102,104],{"class":53,"line":103},5,[51,105,106],{"class":64},")\n",[51,108,110],{"class":53,"line":109},6,[51,111,113],{"emptyLinePlaceholder":112},true,"\n",[51,115,117,120,123,126],{"class":53,"line":116},7,[51,118,119],{"class":68},"with",[51,121,122],{"class":64}," engine.connect() ",[51,124,125],{"class":68},"as",[51,127,128],{"class":64}," conn:\n",[51,130,132,135,138],{"class":53,"line":131},8,[51,133,134],{"class":64},"    conn.execute(text(",[51,136,137],{"class":78},"\"SELECT ...\"",[51,139,140],{"class":64},"))\n",[42,142,144],{"className":44,"code":143,"language":46,"meta":47,"style":47},"# After (SQLAlchemy 2.0 — per-transaction override via execution_options)\nfrom sqlalchemy import create_engine, text\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker\n\n# Engine-wide default (optional — READ COMMITTED is the PostgreSQL default)\nengine = create_engine(\"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\")\n\n# Per-transaction override on a Core Connection\nwith engine.connect() as conn:\n    conn = conn.execution_options(isolation_level=\"REPEATABLE READ\")\n    with conn.begin():\n        result = conn.execute(text(\"SELECT balance FROM accounts WHERE id = 1\"))\n\n# Per-transaction override via ORM Session\nwith Session(engine) as session:\n    session.connection(execution_options={\"isolation_level\": \"SERIALIZABLE\"})\n    with session.begin():\n        account = session.get(Account, 1)\n        account.balance -= 500\n",[18,145,146,151,165,177,189,193,198,212,216,222,233,254,263,279,284,290,303,329,337,354],{"__ignoreMap":47},[51,147,148],{"class":53,"line":54},[51,149,150],{"class":57},"# After (SQLAlchemy 2.0 — per-transaction override via execution_options)\n",[51,152,153,156,159,162],{"class":53,"line":61},[51,154,155],{"class":68},"from",[51,157,158],{"class":64}," sqlalchemy ",[51,160,161],{"class":68},"import",[51,163,164],{"class":64}," create_engine, text\n",[51,166,167,169,172,174],{"class":53,"line":75},[51,168,155],{"class":68},[51,170,171],{"class":64}," sqlalchemy.orm ",[51,173,161],{"class":68},[51,175,176],{"class":64}," Session\n",[51,178,179,181,184,186],{"class":53,"line":85},[51,180,155],{"class":68},[51,182,183],{"class":64}," sqlalchemy.ext.asyncio ",[51,185,161],{"class":68},[51,187,188],{"class":64}," create_async_engine, AsyncSession, async_sessionmaker\n",[51,190,191],{"class":53,"line":103},[51,192,113],{"emptyLinePlaceholder":112},[51,194,195],{"class":53,"line":109},[51,196,197],{"class":57},"# Engine-wide default (optional — READ COMMITTED is the PostgreSQL default)\n",[51,199,200,202,204,207,210],{"class":53,"line":116},[51,201,65],{"class":64},[51,203,69],{"class":68},[51,205,206],{"class":64}," create_engine(",[51,208,209],{"class":78},"\"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\"",[51,211,106],{"class":64},[51,213,214],{"class":53,"line":131},[51,215,113],{"emptyLinePlaceholder":112},[51,217,219],{"class":53,"line":218},9,[51,220,221],{"class":57},"# Per-transaction override on a Core Connection\n",[51,223,225,227,229,231],{"class":53,"line":224},10,[51,226,119],{"class":68},[51,228,122],{"class":64},[51,230,125],{"class":68},[51,232,128],{"class":64},[51,234,236,239,241,244,247,249,252],{"class":53,"line":235},11,[51,237,238],{"class":64},"    conn ",[51,240,69],{"class":68},[51,242,243],{"class":64}," conn.execution_options(",[51,245,246],{"class":88},"isolation_level",[51,248,69],{"class":68},[51,250,251],{"class":78},"\"REPEATABLE READ\"",[51,253,106],{"class":64},[51,255,257,260],{"class":53,"line":256},12,[51,258,259],{"class":68},"    with",[51,261,262],{"class":64}," conn.begin():\n",[51,264,266,269,271,274,277],{"class":53,"line":265},13,[51,267,268],{"class":64},"        result ",[51,270,69],{"class":68},[51,272,273],{"class":64}," conn.execute(text(",[51,275,276],{"class":78},"\"SELECT balance FROM accounts WHERE id = 1\"",[51,278,140],{"class":64},[51,280,282],{"class":53,"line":281},14,[51,283,113],{"emptyLinePlaceholder":112},[51,285,287],{"class":53,"line":286},15,[51,288,289],{"class":57},"# Per-transaction override via ORM Session\n",[51,291,293,295,298,300],{"class":53,"line":292},16,[51,294,119],{"class":68},[51,296,297],{"class":64}," Session(engine) ",[51,299,125],{"class":68},[51,301,302],{"class":64}," session:\n",[51,304,306,309,312,314,317,320,323,326],{"class":53,"line":305},17,[51,307,308],{"class":64},"    session.connection(",[51,310,311],{"class":88},"execution_options",[51,313,69],{"class":68},[51,315,316],{"class":64},"{",[51,318,319],{"class":78},"\"isolation_level\"",[51,321,322],{"class":64},": ",[51,324,325],{"class":78},"\"SERIALIZABLE\"",[51,327,328],{"class":64},"})\n",[51,330,332,334],{"class":53,"line":331},18,[51,333,259],{"class":68},[51,335,336],{"class":64}," session.begin():\n",[51,338,340,343,345,348,352],{"class":53,"line":339},19,[51,341,342],{"class":64},"        account ",[51,344,69],{"class":68},[51,346,347],{"class":64}," session.get(Account, ",[51,349,351],{"class":350},"sj4cs","1",[51,353,106],{"class":64},[51,355,357,360,363],{"class":53,"line":356},20,[51,358,359],{"class":64},"        account.balance ",[51,361,362],{"class":68},"-=",[51,364,365],{"class":350}," 500\n",[14,367,368,369,371,372,376,377,380],{},"The critical rule: ",[18,370,311],{}," must be applied ",[373,374,375],"strong",{},"before the first SQL statement"," in the transaction. After ",[18,378,379],{},"BEGIN"," is issued, the database ignores isolation-level changes until the next transaction.",[37,382,384],{"id":383},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[386,387,389],"h3",{"id":388},"how-execution_options-hooks-into-the-driver","How execution_options Hooks Into the Driver",[14,391,392,393,396,397,400,401,404,405,407],{},"When you call ",[18,394,395],{},"conn.execution_options(isolation_level=\"X\")",", SQLAlchemy does not immediately run a SQL statement. It schedules a DBAPI-level call — ",[18,398,399],{},"connection.set_isolation_level()"," for psycopg2, ",[18,402,403],{},"await connection.execute(\"SET TRANSACTION ISOLATION LEVEL X\")"," for asyncpg — as the first operation when ",[18,406,379],{}," is emitted. The mechanism is dialect-specific but the SQLAlchemy API is uniform.",[14,409,410,411,414],{},"For asyncpg specifically, isolation level is passed as a keyword argument to the asyncpg ",[18,412,413],{},"Transaction"," object at construction time, not via a separate SQL statement. This means the override is zero-cost and race-free even under high concurrency.",[386,416,418],{"id":417},"engine-wide-vs-per-transaction","Engine-Wide vs Per-Transaction",[42,420,422],{"className":44,"code":421,"language":46,"meta":47,"style":47},"# Engine-wide: every connection from this engine uses REPEATABLE READ\nfrom sqlalchemy import create_engine\n\nengine_rr = create_engine(\n    \"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\",\n    isolation_level=\"REPEATABLE READ\",\n)\n\n# Per-transaction: override only for this connection checkout\nwith engine_rr.connect() as conn:\n    # Override engine default to SERIALIZABLE for this one transaction\n    conn = conn.execution_options(isolation_level=\"SERIALIZABLE\")\n    with conn.begin():\n        pass  # runs under SERIALIZABLE\n    # After COMMIT, the connection is returned to pool.\n    # The pool resets isolation to engine default before the next checkout.\n",[18,423,424,429,440,444,453,459,469,473,477,482,493,498,514,520,528,533],{"__ignoreMap":47},[51,425,426],{"class":53,"line":54},[51,427,428],{"class":57},"# Engine-wide: every connection from this engine uses REPEATABLE READ\n",[51,430,431,433,435,437],{"class":53,"line":61},[51,432,155],{"class":68},[51,434,158],{"class":64},[51,436,161],{"class":68},[51,438,439],{"class":64}," create_engine\n",[51,441,442],{"class":53,"line":75},[51,443,113],{"emptyLinePlaceholder":112},[51,445,446,449,451],{"class":53,"line":85},[51,447,448],{"class":64},"engine_rr ",[51,450,69],{"class":68},[51,452,72],{"class":64},[51,454,455,457],{"class":53,"line":103},[51,456,79],{"class":78},[51,458,82],{"class":64},[51,460,461,463,465,467],{"class":53,"line":109},[51,462,89],{"class":88},[51,464,69],{"class":68},[51,466,251],{"class":78},[51,468,82],{"class":64},[51,470,471],{"class":53,"line":116},[51,472,106],{"class":64},[51,474,475],{"class":53,"line":131},[51,476,113],{"emptyLinePlaceholder":112},[51,478,479],{"class":53,"line":218},[51,480,481],{"class":57},"# Per-transaction: override only for this connection checkout\n",[51,483,484,486,489,491],{"class":53,"line":224},[51,485,119],{"class":68},[51,487,488],{"class":64}," engine_rr.connect() ",[51,490,125],{"class":68},[51,492,128],{"class":64},[51,494,495],{"class":53,"line":235},[51,496,497],{"class":57},"    # Override engine default to SERIALIZABLE for this one transaction\n",[51,499,500,502,504,506,508,510,512],{"class":53,"line":256},[51,501,238],{"class":64},[51,503,69],{"class":68},[51,505,243],{"class":64},[51,507,246],{"class":88},[51,509,69],{"class":68},[51,511,325],{"class":78},[51,513,106],{"class":64},[51,515,516,518],{"class":53,"line":265},[51,517,259],{"class":68},[51,519,262],{"class":64},[51,521,522,525],{"class":53,"line":281},[51,523,524],{"class":68},"        pass",[51,526,527],{"class":57},"  # runs under SERIALIZABLE\n",[51,529,530],{"class":53,"line":286},[51,531,532],{"class":57},"    # After COMMIT, the connection is returned to pool.\n",[51,534,535],{"class":53,"line":292},[51,536,537],{"class":57},"    # The pool resets isolation to engine default before the next checkout.\n",[14,539,540],{},"The pool automatically resets the isolation level to the engine default when a connection is returned. You do not need to manually undo the override.",[386,542,544],{"id":543},"orm-session-sessionconnection","ORM Session: session.connection()",[14,546,547,548,551],{},"For ORM workflows the hook is ",[18,549,550],{},"session.connection(execution_options=...)",". Call it before touching any mapped object to ensure the override fires before autobegin.",[42,553,555],{"className":44,"code":554,"language":46,"meta":47,"style":47},"# Sync ORM — REPEATABLE READ before any DML\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import create_engine, select\n\nengine = create_engine(\"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\")\n\ndef aggregate_user_orders(user_id: int) -> int:\n    with Session(engine) as session:\n        # Set isolation BEFORE autobegin fires\n        session.connection(execution_options={\"isolation_level\": \"REPEATABLE READ\"})\n        total = 0\n        # First read establishes the snapshot\n        orders_q1 = session.execute(\n            select(Order).where(Order.user_id == user_id)\n        ).scalars().all()\n        # ... do some processing ...\n        # Second read sees the SAME snapshot — no phantom rows\n        orders_q2 = session.execute(\n            select(Order).where(Order.user_id == user_id)\n        ).scalars().all()\n        assert len(orders_q1) == len(orders_q2), \"Snapshot drifted — isolation broken\"\n        session.rollback()  # read-only; release connection\n        return len(orders_q1)\n",[18,556,557,562,572,583,587,599,603,626,636,641,660,670,675,685,696,701,706,711,720,728,732,754,763],{"__ignoreMap":47},[51,558,559],{"class":53,"line":54},[51,560,561],{"class":57},"# Sync ORM — REPEATABLE READ before any DML\n",[51,563,564,566,568,570],{"class":53,"line":61},[51,565,155],{"class":68},[51,567,171],{"class":64},[51,569,161],{"class":68},[51,571,176],{"class":64},[51,573,574,576,578,580],{"class":53,"line":75},[51,575,155],{"class":68},[51,577,158],{"class":64},[51,579,161],{"class":68},[51,581,582],{"class":64}," create_engine, select\n",[51,584,585],{"class":53,"line":85},[51,586,113],{"emptyLinePlaceholder":112},[51,588,589,591,593,595,597],{"class":53,"line":103},[51,590,65],{"class":64},[51,592,69],{"class":68},[51,594,206],{"class":64},[51,596,209],{"class":78},[51,598,106],{"class":64},[51,600,601],{"class":53,"line":109},[51,602,113],{"emptyLinePlaceholder":112},[51,604,605,608,612,615,618,621,623],{"class":53,"line":116},[51,606,607],{"class":68},"def",[51,609,611],{"class":610},"sScJk"," aggregate_user_orders",[51,613,614],{"class":64},"(user_id: ",[51,616,617],{"class":350},"int",[51,619,620],{"class":64},") -> ",[51,622,617],{"class":350},[51,624,625],{"class":64},":\n",[51,627,628,630,632,634],{"class":53,"line":131},[51,629,259],{"class":68},[51,631,297],{"class":64},[51,633,125],{"class":68},[51,635,302],{"class":64},[51,637,638],{"class":53,"line":218},[51,639,640],{"class":57},"        # Set isolation BEFORE autobegin fires\n",[51,642,643,646,648,650,652,654,656,658],{"class":53,"line":224},[51,644,645],{"class":64},"        session.connection(",[51,647,311],{"class":88},[51,649,69],{"class":68},[51,651,316],{"class":64},[51,653,319],{"class":78},[51,655,322],{"class":64},[51,657,251],{"class":78},[51,659,328],{"class":64},[51,661,662,665,667],{"class":53,"line":235},[51,663,664],{"class":64},"        total ",[51,666,69],{"class":68},[51,668,669],{"class":350}," 0\n",[51,671,672],{"class":53,"line":256},[51,673,674],{"class":57},"        # First read establishes the snapshot\n",[51,676,677,680,682],{"class":53,"line":265},[51,678,679],{"class":64},"        orders_q1 ",[51,681,69],{"class":68},[51,683,684],{"class":64}," session.execute(\n",[51,686,687,690,693],{"class":53,"line":281},[51,688,689],{"class":64},"            select(Order).where(Order.user_id ",[51,691,692],{"class":68},"==",[51,694,695],{"class":64}," user_id)\n",[51,697,698],{"class":53,"line":286},[51,699,700],{"class":64},"        ).scalars().all()\n",[51,702,703],{"class":53,"line":292},[51,704,705],{"class":57},"        # ... do some processing ...\n",[51,707,708],{"class":53,"line":305},[51,709,710],{"class":57},"        # Second read sees the SAME snapshot — no phantom rows\n",[51,712,713,716,718],{"class":53,"line":331},[51,714,715],{"class":64},"        orders_q2 ",[51,717,69],{"class":68},[51,719,684],{"class":64},[51,721,722,724,726],{"class":53,"line":339},[51,723,689],{"class":64},[51,725,692],{"class":68},[51,727,695],{"class":64},[51,729,730],{"class":53,"line":356},[51,731,700],{"class":64},[51,733,735,738,741,744,746,748,751],{"class":53,"line":734},21,[51,736,737],{"class":68},"        assert",[51,739,740],{"class":350}," len",[51,742,743],{"class":64},"(orders_q1) ",[51,745,692],{"class":68},[51,747,740],{"class":350},[51,749,750],{"class":64},"(orders_q2), ",[51,752,753],{"class":78},"\"Snapshot drifted — isolation broken\"\n",[51,755,757,760],{"class":53,"line":756},22,[51,758,759],{"class":64},"        session.rollback()  ",[51,761,762],{"class":57},"# read-only; release connection\n",[51,764,766,769,771],{"class":53,"line":765},23,[51,767,768],{"class":68},"        return",[51,770,740],{"class":350},[51,772,773],{"class":64},"(orders_q1)\n",[42,775,777],{"className":44,"code":776,"language":46,"meta":47,"style":47},"# Async ORM — await session.connection() before first statement\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy import select\n\nasync_engine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Forders\")\nAsyncSessionFactory = async_sessionmaker(async_engine, expire_on_commit=False)\n\nasync def aggregate_user_orders_async(user_id: int) -> int:\n    async with AsyncSessionFactory() as session:\n        # Must await — asyncpg sets isolation asynchronously\n        await session.connection(execution_options={\"isolation_level\": \"REPEATABLE READ\"})\n        orders = (await session.execute(\n            select(Order).where(Order.user_id == user_id)\n        )).scalars().all()\n        await session.rollback()\n        return len(orders)\n",[18,778,779,784,795,806,810,825,845,849,870,885,890,912,927,935,940,947],{"__ignoreMap":47},[51,780,781],{"class":53,"line":54},[51,782,783],{"class":57},"# Async ORM — await session.connection() before first statement\n",[51,785,786,788,790,792],{"class":53,"line":61},[51,787,155],{"class":68},[51,789,183],{"class":64},[51,791,161],{"class":68},[51,793,794],{"class":64}," AsyncSession, create_async_engine, async_sessionmaker\n",[51,796,797,799,801,803],{"class":53,"line":75},[51,798,155],{"class":68},[51,800,158],{"class":64},[51,802,161],{"class":68},[51,804,805],{"class":64}," select\n",[51,807,808],{"class":53,"line":85},[51,809,113],{"emptyLinePlaceholder":112},[51,811,812,815,817,820,823],{"class":53,"line":103},[51,813,814],{"class":64},"async_engine ",[51,816,69],{"class":68},[51,818,819],{"class":64}," create_async_engine(",[51,821,822],{"class":78},"\"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Forders\"",[51,824,106],{"class":64},[51,826,827,830,832,835,838,840,843],{"class":53,"line":109},[51,828,829],{"class":64},"AsyncSessionFactory ",[51,831,69],{"class":68},[51,833,834],{"class":64}," async_sessionmaker(async_engine, ",[51,836,837],{"class":88},"expire_on_commit",[51,839,69],{"class":68},[51,841,842],{"class":350},"False",[51,844,106],{"class":64},[51,846,847],{"class":53,"line":116},[51,848,113],{"emptyLinePlaceholder":112},[51,850,851,854,857,860,862,864,866,868],{"class":53,"line":131},[51,852,853],{"class":68},"async",[51,855,856],{"class":68}," def",[51,858,859],{"class":610}," aggregate_user_orders_async",[51,861,614],{"class":64},[51,863,617],{"class":350},[51,865,620],{"class":64},[51,867,617],{"class":350},[51,869,625],{"class":64},[51,871,872,875,878,881,883],{"class":53,"line":218},[51,873,874],{"class":68},"    async",[51,876,877],{"class":68}," with",[51,879,880],{"class":64}," AsyncSessionFactory() ",[51,882,125],{"class":68},[51,884,302],{"class":64},[51,886,887],{"class":53,"line":224},[51,888,889],{"class":57},"        # Must await — asyncpg sets isolation asynchronously\n",[51,891,892,895,898,900,902,904,906,908,910],{"class":53,"line":235},[51,893,894],{"class":68},"        await",[51,896,897],{"class":64}," session.connection(",[51,899,311],{"class":88},[51,901,69],{"class":68},[51,903,316],{"class":64},[51,905,319],{"class":78},[51,907,322],{"class":64},[51,909,251],{"class":78},[51,911,328],{"class":64},[51,913,914,917,919,922,925],{"class":53,"line":256},[51,915,916],{"class":64},"        orders ",[51,918,69],{"class":68},[51,920,921],{"class":64}," (",[51,923,924],{"class":68},"await",[51,926,684],{"class":64},[51,928,929,931,933],{"class":53,"line":265},[51,930,689],{"class":64},[51,932,692],{"class":68},[51,934,695],{"class":64},[51,936,937],{"class":53,"line":281},[51,938,939],{"class":64},"        )).scalars().all()\n",[51,941,942,944],{"class":53,"line":286},[51,943,894],{"class":68},[51,945,946],{"class":64}," session.rollback()\n",[51,948,949,951,953],{"class":53,"line":292},[51,950,768],{"class":68},[51,952,740],{"class":350},[51,954,955],{"class":64},"(orders)\n",[386,957,959],{"id":958},"verifying-the-active-isolation-level-in-tests","Verifying the Active Isolation Level in Tests",[14,961,962],{},"The most common source of isolation-related bugs is believing your override is in effect when it is not. Add an assertion in your test suite:",[42,964,966],{"className":44,"code":965,"language":46,"meta":47,"style":47},"import pytest\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy import text\n\nasync_engine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Ftest_db\")\nTestSessionFactory = async_sessionmaker(async_engine, expire_on_commit=False)\n\n@pytest.mark.asyncio\nasync def test_isolation_level_is_serializable() -> None:\n    async with TestSessionFactory() as session:\n        await session.connection(execution_options={\"isolation_level\": \"SERIALIZABLE\"})\n        # Confirm PostgreSQL actually sees SERIALIZABLE\n        row = (await session.execute(\n            text(\"SELECT current_setting('transaction_isolation')\")\n        )).scalar_one()\n        assert row == \"serializable\", f\"Expected serializable, got {row!r}\"\n        await session.rollback()\n",[18,967,968,975,985,996,1000,1013,1030,1034,1039,1056,1069,1089,1094,1107,1117,1122,1157],{"__ignoreMap":47},[51,969,970,972],{"class":53,"line":54},[51,971,161],{"class":68},[51,973,974],{"class":64}," pytest\n",[51,976,977,979,981,983],{"class":53,"line":61},[51,978,155],{"class":68},[51,980,183],{"class":64},[51,982,161],{"class":68},[51,984,794],{"class":64},[51,986,987,989,991,993],{"class":53,"line":75},[51,988,155],{"class":68},[51,990,158],{"class":64},[51,992,161],{"class":68},[51,994,995],{"class":64}," text\n",[51,997,998],{"class":53,"line":85},[51,999,113],{"emptyLinePlaceholder":112},[51,1001,1002,1004,1006,1008,1011],{"class":53,"line":103},[51,1003,814],{"class":64},[51,1005,69],{"class":68},[51,1007,819],{"class":64},[51,1009,1010],{"class":78},"\"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Ftest_db\"",[51,1012,106],{"class":64},[51,1014,1015,1018,1020,1022,1024,1026,1028],{"class":53,"line":109},[51,1016,1017],{"class":64},"TestSessionFactory ",[51,1019,69],{"class":68},[51,1021,834],{"class":64},[51,1023,837],{"class":88},[51,1025,69],{"class":68},[51,1027,842],{"class":350},[51,1029,106],{"class":64},[51,1031,1032],{"class":53,"line":116},[51,1033,113],{"emptyLinePlaceholder":112},[51,1035,1036],{"class":53,"line":131},[51,1037,1038],{"class":610},"@pytest.mark.asyncio\n",[51,1040,1041,1043,1045,1048,1051,1054],{"class":53,"line":218},[51,1042,853],{"class":68},[51,1044,856],{"class":68},[51,1046,1047],{"class":610}," test_isolation_level_is_serializable",[51,1049,1050],{"class":64},"() -> ",[51,1052,1053],{"class":350},"None",[51,1055,625],{"class":64},[51,1057,1058,1060,1062,1065,1067],{"class":53,"line":224},[51,1059,874],{"class":68},[51,1061,877],{"class":68},[51,1063,1064],{"class":64}," TestSessionFactory() ",[51,1066,125],{"class":68},[51,1068,302],{"class":64},[51,1070,1071,1073,1075,1077,1079,1081,1083,1085,1087],{"class":53,"line":235},[51,1072,894],{"class":68},[51,1074,897],{"class":64},[51,1076,311],{"class":88},[51,1078,69],{"class":68},[51,1080,316],{"class":64},[51,1082,319],{"class":78},[51,1084,322],{"class":64},[51,1086,325],{"class":78},[51,1088,328],{"class":64},[51,1090,1091],{"class":53,"line":256},[51,1092,1093],{"class":57},"        # Confirm PostgreSQL actually sees SERIALIZABLE\n",[51,1095,1096,1099,1101,1103,1105],{"class":53,"line":265},[51,1097,1098],{"class":64},"        row ",[51,1100,69],{"class":68},[51,1102,921],{"class":64},[51,1104,924],{"class":68},[51,1106,684],{"class":64},[51,1108,1109,1112,1115],{"class":53,"line":281},[51,1110,1111],{"class":64},"            text(",[51,1113,1114],{"class":78},"\"SELECT current_setting('transaction_isolation')\"",[51,1116,106],{"class":64},[51,1118,1119],{"class":53,"line":286},[51,1120,1121],{"class":64},"        )).scalar_one()\n",[51,1123,1124,1126,1129,1131,1134,1137,1140,1143,1145,1148,1151,1154],{"class":53,"line":292},[51,1125,737],{"class":68},[51,1127,1128],{"class":64}," row ",[51,1130,692],{"class":68},[51,1132,1133],{"class":78}," \"serializable\"",[51,1135,1136],{"class":64},", ",[51,1138,1139],{"class":68},"f",[51,1141,1142],{"class":78},"\"Expected serializable, got ",[51,1144,316],{"class":350},[51,1146,1147],{"class":64},"row",[51,1149,1150],{"class":68},"!r",[51,1152,1153],{"class":350},"}",[51,1155,1156],{"class":78},"\"\n",[51,1158,1159,1161],{"class":53,"line":305},[51,1160,894],{"class":68},[51,1162,946],{"class":64},[14,1164,1165,1166,1169],{},"Run this in CI alongside your integration tests. It catches misconfigured engines, driver version mismatches, and PgBouncer transaction-pooling environments where ",[18,1167,1168],{},"SET"," commands may be silently dropped.",[386,1171,1173],{"id":1172},"isolation-level-strings-by-dialect","Isolation Level Strings by Dialect",[14,1175,1176,1177,1180],{},"SQLAlchemy validates isolation level strings against a per-dialect allowlist. Using an incorrect string raises ",[18,1178,1179],{},"ArgumentError"," at configuration time (not at runtime), which makes it easy to catch in tests.",[1182,1183,1184,1197],"table",{},[1185,1186,1187],"thead",{},[1188,1189,1190,1194],"tr",{},[1191,1192,1193],"th",{},"Dialect",[1191,1195,1196],{},"Valid isolation level strings",[1198,1199,1200,1218,1234,1252],"tbody",{},[1188,1201,1202,1206],{},[1203,1204,1205],"td",{},"PostgreSQL (psycopg2, asyncpg)",[1203,1207,1208,1136,1211,1136,1213,1136,1215],{},[18,1209,1210],{},"\"READ COMMITTED\"",[18,1212,251],{},[18,1214,325],{},[18,1216,1217],{},"\"AUTOCOMMIT\"",[1188,1219,1220,1223],{},[1203,1221,1222],{},"MySQL (mysqlclient, aiomysql)",[1203,1224,1225,1136,1228,1136,1230,1136,1232],{},[18,1226,1227],{},"\"READ UNCOMMITTED\"",[18,1229,1210],{},[18,1231,251],{},[18,1233,325],{},[1188,1235,1236,1239],{},[1203,1237,1238],{},"SQLite (pysqlite, aiosqlite)",[1203,1240,1241,1136,1244,1136,1247,1136,1250],{},[18,1242,1243],{},"\"DEFERRED\"",[18,1245,1246],{},"\"IMMEDIATE\"",[18,1248,1249],{},"\"EXCLUSIVE\"",[18,1251,1217],{},[1188,1253,1254,1257],{},[1203,1255,1256],{},"Microsoft SQL Server (pyodbc)",[1203,1258,1259,1136,1261,1136,1263,1136,1265,1136,1267],{},[18,1260,1227],{},[18,1262,1210],{},[18,1264,251],{},[18,1266,325],{},[18,1268,1269],{},"\"SNAPSHOT\"",[14,1271,1272,1273,1276,1277,1279,1280,1283],{},"Note that PostgreSQL uses ",[373,1274,1275],{},"spaces"," in the string (",[18,1278,1210],{},") while MySQL uses ",[373,1281,1282],{},"underscores"," in its native syntax but SQLAlchemy normalises both — use the space-separated form for portability across SQLAlchemy's dialect layer.",[386,1285,1287],{"id":1286},"per-sessionmaker-default","Per-Sessionmaker Default",[14,1289,1290,1291,1294],{},"If every session created by a factory should use a non-default isolation level, configure it on the engine passed to ",[18,1292,1293],{},"async_sessionmaker",":",[42,1296,1298],{"className":44,"code":1297,"language":46,"meta":47,"style":47},"from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n\n# All sessions from this factory use REPEATABLE READ\nreporting_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fro_user:secret@replica.db\u002Forders\",\n    isolation_level=\"REPEATABLE READ\",\n    pool_size=5,\n    max_overflow=2,\n)\nReportingSessionFactory = async_sessionmaker(reporting_engine, expire_on_commit=False)\n\nasync def run_report() -> list[dict]:\n    async with ReportingSessionFactory() as session:\n        result = await session.execute(select(Order))\n        return [{\"id\": o.id, \"total\": o.total_cents} for o in result.scalars()]\n",[18,1299,1300,1311,1315,1320,1330,1337,1347,1359,1371,1375,1393,1397,1415,1428,1440],{"__ignoreMap":47},[51,1301,1302,1304,1306,1308],{"class":53,"line":54},[51,1303,155],{"class":68},[51,1305,183],{"class":64},[51,1307,161],{"class":68},[51,1309,1310],{"class":64}," create_async_engine, async_sessionmaker, AsyncSession\n",[51,1312,1313],{"class":53,"line":61},[51,1314,113],{"emptyLinePlaceholder":112},[51,1316,1317],{"class":53,"line":75},[51,1318,1319],{"class":57},"# All sessions from this factory use REPEATABLE READ\n",[51,1321,1322,1325,1327],{"class":53,"line":85},[51,1323,1324],{"class":64},"reporting_engine ",[51,1326,69],{"class":68},[51,1328,1329],{"class":64}," create_async_engine(\n",[51,1331,1332,1335],{"class":53,"line":103},[51,1333,1334],{"class":78},"    \"postgresql+asyncpg:\u002F\u002Fro_user:secret@replica.db\u002Forders\"",[51,1336,82],{"class":64},[51,1338,1339,1341,1343,1345],{"class":53,"line":109},[51,1340,89],{"class":88},[51,1342,69],{"class":68},[51,1344,251],{"class":78},[51,1346,82],{"class":64},[51,1348,1349,1352,1354,1357],{"class":53,"line":116},[51,1350,1351],{"class":88},"    pool_size",[51,1353,69],{"class":68},[51,1355,1356],{"class":350},"5",[51,1358,82],{"class":64},[51,1360,1361,1364,1366,1369],{"class":53,"line":131},[51,1362,1363],{"class":88},"    max_overflow",[51,1365,69],{"class":68},[51,1367,1368],{"class":350},"2",[51,1370,82],{"class":64},[51,1372,1373],{"class":53,"line":218},[51,1374,106],{"class":64},[51,1376,1377,1380,1382,1385,1387,1389,1391],{"class":53,"line":224},[51,1378,1379],{"class":64},"ReportingSessionFactory ",[51,1381,69],{"class":68},[51,1383,1384],{"class":64}," async_sessionmaker(reporting_engine, ",[51,1386,837],{"class":88},[51,1388,69],{"class":68},[51,1390,842],{"class":350},[51,1392,106],{"class":64},[51,1394,1395],{"class":53,"line":235},[51,1396,113],{"emptyLinePlaceholder":112},[51,1398,1399,1401,1403,1406,1409,1412],{"class":53,"line":256},[51,1400,853],{"class":68},[51,1402,856],{"class":68},[51,1404,1405],{"class":610}," run_report",[51,1407,1408],{"class":64},"() -> list[",[51,1410,1411],{"class":350},"dict",[51,1413,1414],{"class":64},"]:\n",[51,1416,1417,1419,1421,1424,1426],{"class":53,"line":265},[51,1418,874],{"class":68},[51,1420,877],{"class":68},[51,1422,1423],{"class":64}," ReportingSessionFactory() ",[51,1425,125],{"class":68},[51,1427,302],{"class":64},[51,1429,1430,1432,1434,1437],{"class":53,"line":281},[51,1431,268],{"class":64},[51,1433,69],{"class":68},[51,1435,1436],{"class":68}," await",[51,1438,1439],{"class":64}," session.execute(select(Order))\n",[51,1441,1442,1444,1447,1450,1453,1456,1459,1462,1465,1468],{"class":53,"line":286},[51,1443,768],{"class":68},[51,1445,1446],{"class":64}," [{",[51,1448,1449],{"class":78},"\"id\"",[51,1451,1452],{"class":64},": o.id, ",[51,1454,1455],{"class":78},"\"total\"",[51,1457,1458],{"class":64},": o.total_cents} ",[51,1460,1461],{"class":68},"for",[51,1463,1464],{"class":64}," o ",[51,1466,1467],{"class":68},"in",[51,1469,1470],{"class":64}," result.scalars()]\n",[14,1472,1473],{},"Using a dedicated reporting engine with its own pool and isolation level cleanly separates read-heavy analytical workloads from transactional OLTP sessions.",[37,1475,1477],{"id":1476},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1182,1479,1480,1493],{},[1185,1481,1482],{},[1188,1483,1484,1487,1490],{},[1191,1485,1486],{},"Error \u002F Warning",[1191,1488,1489],{},"Root Cause",[1191,1491,1492],{},"Production Fix",[1198,1494,1495,1516,1543,1560,1593,1623,1647],{},[1188,1496,1497,1505,1511],{},[1203,1498,1499,1502,1503],{},[18,1500,1501],{},"sqlalchemy.exc.InvalidRequestError: Transaction already started"," when calling ",[18,1504,311],{},[1203,1506,1507,1508,1510],{},"A statement was executed before ",[18,1509,311],{}," was set, firing autobegin",[1203,1512,16,1513,1515],{},[18,1514,550],{}," as the very first operation on the session",[1188,1517,1518,1523,1535],{},[1203,1519,1520],{},[18,1521,1522],{},"psycopg2.errors.ActiveSqlTransaction: SET TRANSACTION ISOLATION LEVEL must be called before any query",[1203,1524,1525,1527,1528,1530,1531,1534],{},[18,1526,311],{}," was applied after ",[18,1529,379],{}," via a raw ",[18,1532,1533],{},"SET TRANSACTION"," call",[1203,1536,1537,1538,1540,1541],{},"Use SQLAlchemy's ",[18,1539,311],{}," hook, not raw SQL; it fires before ",[18,1542,379],{},[1188,1544,1545,1550,1553],{},[1203,1546,1547],{},[18,1548,1549],{},"asyncpg.exceptions.ActiveSQLTransactionError",[1203,1551,1552],{},"Same as above on asyncpg",[1203,1554,1555,1556,1559],{},"Ensure ",[18,1557,1558],{},"await session.connection(execution_options=...)"," precedes any DML or SELECT",[1188,1561,1562,1565,1578],{},[1203,1563,1564],{},"Silent fallback to engine default isolation level",[1203,1566,1567,1570,1571,1574,1575],{},[18,1568,1569],{},"execution_options(isolation_level=...)"," was called on the ",[18,1572,1573],{},"Engine"," object itself, not on a ",[18,1576,1577],{},"Connection",[1203,1579,16,1580,1582,1583,1585,1586,1589,1590],{},[18,1581,311],{}," on the ",[18,1584,1577],{}," returned by ",[18,1587,1588],{},"engine.connect()",", not on ",[18,1591,1592],{},"engine",[1188,1594,1595,1601,1612],{},[1203,1596,1597,1600],{},[18,1598,1599],{},"pool.reset_on_return"," overwrite issue — isolation not reset between requests",[1203,1602,1603,1604,1607,1608,1611],{},"Using ",[18,1605,1606],{},"NullPool"," or ",[18,1609,1610],{},"StaticPool"," where connection is not returned to pool",[1203,1613,1614,1615,1618,1619,1622],{},"Use ",[18,1616,1617],{},"QueuePool"," (default) or call ",[18,1620,1621],{},"conn.execution_options(isolation_level=\u003Cengine_default>)"," on reuse",[1188,1624,1625,1631,1634],{},[1203,1626,1627,1630],{},[18,1628,1629],{},"StaleDataError"," raised unexpectedly under SERIALIZABLE",[1203,1632,1633],{},"Concurrent transaction modified a row between your read and your write; optimistic-lock version mismatch",[1203,1635,1636,1637,1607,1639,1642,1643,1646],{},"Catch ",[18,1638,1629],{},[18,1640,1641],{},"OperationalError"," with SQLSTATE ",[18,1644,1645],{},"40001","; retry the full transaction from a new session",[1188,1648,1649,1657,1663],{},[1203,1650,1651,1653,1654],{},[18,1652,246],{}," string not recognised — ",[18,1655,1656],{},"OperationalError: unknown isolation level",[1203,1658,1659,1660,1662],{},"Dialect-specific string mismatch (e.g. ",[18,1661,94],{}," with underscore on PostgreSQL)",[1203,1664,1665,1666,1136,1668,1136,1670,1672,1673],{},"PostgreSQL uses spaces: ",[18,1667,251],{},[18,1669,1210],{},[18,1671,325],{},". MySQL uses underscores: ",[18,1674,94],{},[37,1676,1678],{"id":1677},"advanced-isolation-optimization","Advanced Isolation Optimization",[386,1680,1682],{"id":1681},"the-autocommit-pseudo-isolation-level","The AUTOCOMMIT Pseudo-Isolation Level",[14,1684,1685,1686,1688,1689,1691,1692,1695],{},"SQLAlchemy exposes ",[18,1687,1217],{}," as an isolation level string. Setting it bypasses SQLAlchemy's transaction management entirely — no ",[18,1690,379],{}," is issued, and each statement is committed immediately by the DBAPI. This is appropriate for DDL operations that must run outside a transaction (e.g., ",[18,1693,1694],{},"CREATE INDEX CONCURRENTLY"," on PostgreSQL, which cannot run inside a transaction block) and for calling stored procedures that manage their own transaction state.",[42,1697,1699],{"className":44,"code":1698,"language":46,"meta":47,"style":47},"from sqlalchemy import create_engine, text\n\nengine = create_engine(\"postgresql+psycopg2:\u002F\u002Fapp:secret@localhost\u002Forders\")\n\ndef create_index_concurrently(table: str, column: str) -> None:\n    \"\"\"CREATE INDEX CONCURRENTLY must run outside a transaction block.\"\"\"\n    with engine.connect() as conn:\n        conn = conn.execution_options(isolation_level=\"AUTOCOMMIT\")\n        # No BEGIN issued; each statement commits immediately\n        conn.execute(\n            text(f\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_{table}_{column} ON {table} ({column})\")\n        )\n    # Connection returned to pool; pool resets to engine default (not AUTOCOMMIT)\n",[18,1700,1701,1711,1715,1727,1731,1755,1760,1770,1787,1792,1797,1844,1849],{"__ignoreMap":47},[51,1702,1703,1705,1707,1709],{"class":53,"line":54},[51,1704,155],{"class":68},[51,1706,158],{"class":64},[51,1708,161],{"class":68},[51,1710,164],{"class":64},[51,1712,1713],{"class":53,"line":61},[51,1714,113],{"emptyLinePlaceholder":112},[51,1716,1717,1719,1721,1723,1725],{"class":53,"line":75},[51,1718,65],{"class":64},[51,1720,69],{"class":68},[51,1722,206],{"class":64},[51,1724,209],{"class":78},[51,1726,106],{"class":64},[51,1728,1729],{"class":53,"line":85},[51,1730,113],{"emptyLinePlaceholder":112},[51,1732,1733,1735,1738,1741,1744,1747,1749,1751,1753],{"class":53,"line":103},[51,1734,607],{"class":68},[51,1736,1737],{"class":610}," create_index_concurrently",[51,1739,1740],{"class":64},"(table: ",[51,1742,1743],{"class":350},"str",[51,1745,1746],{"class":64},", column: ",[51,1748,1743],{"class":350},[51,1750,620],{"class":64},[51,1752,1053],{"class":350},[51,1754,625],{"class":64},[51,1756,1757],{"class":53,"line":109},[51,1758,1759],{"class":78},"    \"\"\"CREATE INDEX CONCURRENTLY must run outside a transaction block.\"\"\"\n",[51,1761,1762,1764,1766,1768],{"class":53,"line":116},[51,1763,259],{"class":68},[51,1765,122],{"class":64},[51,1767,125],{"class":68},[51,1769,128],{"class":64},[51,1771,1772,1775,1777,1779,1781,1783,1785],{"class":53,"line":131},[51,1773,1774],{"class":64},"        conn ",[51,1776,69],{"class":68},[51,1778,243],{"class":64},[51,1780,246],{"class":88},[51,1782,69],{"class":68},[51,1784,1217],{"class":78},[51,1786,106],{"class":64},[51,1788,1789],{"class":53,"line":218},[51,1790,1791],{"class":57},"        # No BEGIN issued; each statement commits immediately\n",[51,1793,1794],{"class":53,"line":224},[51,1795,1796],{"class":64},"        conn.execute(\n",[51,1798,1799,1801,1803,1806,1808,1810,1812,1815,1817,1820,1822,1825,1827,1829,1831,1833,1835,1837,1839,1842],{"class":53,"line":235},[51,1800,1111],{"class":64},[51,1802,1139],{"class":68},[51,1804,1805],{"class":78},"\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_",[51,1807,316],{"class":350},[51,1809,1182],{"class":64},[51,1811,1153],{"class":350},[51,1813,1814],{"class":78},"_",[51,1816,316],{"class":350},[51,1818,1819],{"class":64},"column",[51,1821,1153],{"class":350},[51,1823,1824],{"class":78}," ON ",[51,1826,316],{"class":350},[51,1828,1182],{"class":64},[51,1830,1153],{"class":350},[51,1832,921],{"class":78},[51,1834,316],{"class":350},[51,1836,1819],{"class":64},[51,1838,1153],{"class":350},[51,1840,1841],{"class":78},")\"",[51,1843,106],{"class":64},[51,1845,1846],{"class":53,"line":256},[51,1847,1848],{"class":64},"        )\n",[51,1850,1851],{"class":53,"line":265},[51,1852,1853],{"class":57},"    # Connection returned to pool; pool resets to engine default (not AUTOCOMMIT)\n",[14,1855,1856,1857,1860],{},"Do not use ",[18,1858,1859],{},"AUTOCOMMIT"," for normal application reads or writes. Without transaction boundaries, partial writes cannot be rolled back and constraint violations leave the database in an inconsistent state.",[386,1862,1864],{"id":1863},"mixing-isolation-levels-within-a-single-request","Mixing Isolation Levels Within a Single Request",[14,1866,1867,1868,1871,1872,1875],{},"A common pattern in financial applications is to use ",[18,1869,1870],{},"READ COMMITTED"," for the bulk of a request (lowest overhead, best concurrency) and escalate to ",[18,1873,1874],{},"SERIALIZABLE"," only for the critical write path. Because isolation level is per-transaction, you can do this within a single HTTP request using two separate sessions that each open and close their own connection:",[42,1877,1879],{"className":44,"code":1878,"language":46,"meta":47,"style":47},"from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\nfrom sqlalchemy import select, text\n\nasync_engine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Forders\")\nSessionFactory = async_sessionmaker(async_engine, expire_on_commit=False)\n\nasync def checkout(cart_id: int, user_id: int) -> dict:\n    # Phase 1: read cart contents under READ COMMITTED (default)\n    async with SessionFactory() as read_session:\n        cart_items = (await read_session.execute(\n            select(CartItem).where(CartItem.cart_id == cart_id)\n        )).scalars().all()\n\n    # Phase 2: commit the order under SERIALIZABLE to prevent double-booking\n    async with SessionFactory() as write_session:\n        await write_session.connection(\n            execution_options={\"isolation_level\": \"SERIALIZABLE\"}\n        )\n        async with write_session.begin():\n            for item in cart_items:\n                product = await write_session.get(Product, item.product_id)\n                if product.stock_qty \u003C item.quantity:\n                    raise ValueError(f\"Insufficient stock for {product.name}\")\n                product.stock_qty -= item.quantity\n            order = Order(user_id=user_id, status=\"confirmed\")\n            write_session.add(order)\n\n    return {\"order_id\": order.id, \"items\": len(cart_items)}\n",[18,1880,1881,1892,1903,1907,1919,1936,1940,1965,1970,1984,1998,2008,2012,2016,2021,2034,2041,2059,2063,2073,2086,2098,2112,2140,2151,2180,2186,2191],{"__ignoreMap":47},[51,1882,1883,1885,1887,1889],{"class":53,"line":54},[51,1884,155],{"class":68},[51,1886,183],{"class":64},[51,1888,161],{"class":68},[51,1890,1891],{"class":64}," AsyncSession, async_sessionmaker, create_async_engine\n",[51,1893,1894,1896,1898,1900],{"class":53,"line":61},[51,1895,155],{"class":68},[51,1897,158],{"class":64},[51,1899,161],{"class":68},[51,1901,1902],{"class":64}," select, text\n",[51,1904,1905],{"class":53,"line":75},[51,1906,113],{"emptyLinePlaceholder":112},[51,1908,1909,1911,1913,1915,1917],{"class":53,"line":85},[51,1910,814],{"class":64},[51,1912,69],{"class":68},[51,1914,819],{"class":64},[51,1916,822],{"class":78},[51,1918,106],{"class":64},[51,1920,1921,1924,1926,1928,1930,1932,1934],{"class":53,"line":103},[51,1922,1923],{"class":64},"SessionFactory ",[51,1925,69],{"class":68},[51,1927,834],{"class":64},[51,1929,837],{"class":88},[51,1931,69],{"class":68},[51,1933,842],{"class":350},[51,1935,106],{"class":64},[51,1937,1938],{"class":53,"line":109},[51,1939,113],{"emptyLinePlaceholder":112},[51,1941,1942,1944,1946,1949,1952,1954,1957,1959,1961,1963],{"class":53,"line":116},[51,1943,853],{"class":68},[51,1945,856],{"class":68},[51,1947,1948],{"class":610}," checkout",[51,1950,1951],{"class":64},"(cart_id: ",[51,1953,617],{"class":350},[51,1955,1956],{"class":64},", user_id: ",[51,1958,617],{"class":350},[51,1960,620],{"class":64},[51,1962,1411],{"class":350},[51,1964,625],{"class":64},[51,1966,1967],{"class":53,"line":131},[51,1968,1969],{"class":57},"    # Phase 1: read cart contents under READ COMMITTED (default)\n",[51,1971,1972,1974,1976,1979,1981],{"class":53,"line":218},[51,1973,874],{"class":68},[51,1975,877],{"class":68},[51,1977,1978],{"class":64}," SessionFactory() ",[51,1980,125],{"class":68},[51,1982,1983],{"class":64}," read_session:\n",[51,1985,1986,1989,1991,1993,1995],{"class":53,"line":224},[51,1987,1988],{"class":64},"        cart_items ",[51,1990,69],{"class":68},[51,1992,921],{"class":64},[51,1994,924],{"class":68},[51,1996,1997],{"class":64}," read_session.execute(\n",[51,1999,2000,2003,2005],{"class":53,"line":235},[51,2001,2002],{"class":64},"            select(CartItem).where(CartItem.cart_id ",[51,2004,692],{"class":68},[51,2006,2007],{"class":64}," cart_id)\n",[51,2009,2010],{"class":53,"line":256},[51,2011,939],{"class":64},[51,2013,2014],{"class":53,"line":265},[51,2015,113],{"emptyLinePlaceholder":112},[51,2017,2018],{"class":53,"line":281},[51,2019,2020],{"class":57},"    # Phase 2: commit the order under SERIALIZABLE to prevent double-booking\n",[51,2022,2023,2025,2027,2029,2031],{"class":53,"line":286},[51,2024,874],{"class":68},[51,2026,877],{"class":68},[51,2028,1978],{"class":64},[51,2030,125],{"class":68},[51,2032,2033],{"class":64}," write_session:\n",[51,2035,2036,2038],{"class":53,"line":292},[51,2037,894],{"class":68},[51,2039,2040],{"class":64}," write_session.connection(\n",[51,2042,2043,2046,2048,2050,2052,2054,2056],{"class":53,"line":305},[51,2044,2045],{"class":88},"            execution_options",[51,2047,69],{"class":68},[51,2049,316],{"class":64},[51,2051,319],{"class":78},[51,2053,322],{"class":64},[51,2055,325],{"class":78},[51,2057,2058],{"class":64},"}\n",[51,2060,2061],{"class":53,"line":331},[51,2062,1848],{"class":64},[51,2064,2065,2068,2070],{"class":53,"line":339},[51,2066,2067],{"class":68},"        async",[51,2069,877],{"class":68},[51,2071,2072],{"class":64}," write_session.begin():\n",[51,2074,2075,2078,2081,2083],{"class":53,"line":356},[51,2076,2077],{"class":68},"            for",[51,2079,2080],{"class":64}," item ",[51,2082,1467],{"class":68},[51,2084,2085],{"class":64}," cart_items:\n",[51,2087,2088,2091,2093,2095],{"class":53,"line":734},[51,2089,2090],{"class":64},"                product ",[51,2092,69],{"class":68},[51,2094,1436],{"class":68},[51,2096,2097],{"class":64}," write_session.get(Product, item.product_id)\n",[51,2099,2100,2103,2106,2109],{"class":53,"line":756},[51,2101,2102],{"class":68},"                if",[51,2104,2105],{"class":64}," product.stock_qty ",[51,2107,2108],{"class":68},"\u003C",[51,2110,2111],{"class":64}," item.quantity:\n",[51,2113,2114,2117,2120,2123,2125,2128,2130,2133,2135,2138],{"class":53,"line":765},[51,2115,2116],{"class":68},"                    raise",[51,2118,2119],{"class":350}," ValueError",[51,2121,2122],{"class":64},"(",[51,2124,1139],{"class":68},[51,2126,2127],{"class":78},"\"Insufficient stock for ",[51,2129,316],{"class":350},[51,2131,2132],{"class":64},"product.name",[51,2134,1153],{"class":350},[51,2136,2137],{"class":78},"\"",[51,2139,106],{"class":64},[51,2141,2143,2146,2148],{"class":53,"line":2142},24,[51,2144,2145],{"class":64},"                product.stock_qty ",[51,2147,362],{"class":68},[51,2149,2150],{"class":64}," item.quantity\n",[51,2152,2154,2157,2159,2162,2165,2167,2170,2173,2175,2178],{"class":53,"line":2153},25,[51,2155,2156],{"class":64},"            order ",[51,2158,69],{"class":68},[51,2160,2161],{"class":64}," Order(",[51,2163,2164],{"class":88},"user_id",[51,2166,69],{"class":68},[51,2168,2169],{"class":64},"user_id, ",[51,2171,2172],{"class":88},"status",[51,2174,69],{"class":68},[51,2176,2177],{"class":78},"\"confirmed\"",[51,2179,106],{"class":64},[51,2181,2183],{"class":53,"line":2182},26,[51,2184,2185],{"class":64},"            write_session.add(order)\n",[51,2187,2189],{"class":53,"line":2188},27,[51,2190,113],{"emptyLinePlaceholder":112},[51,2192,2194,2197,2200,2203,2206,2209,2211,2214],{"class":53,"line":2193},28,[51,2195,2196],{"class":68},"    return",[51,2198,2199],{"class":64}," {",[51,2201,2202],{"class":78},"\"order_id\"",[51,2204,2205],{"class":64},": order.id, ",[51,2207,2208],{"class":78},"\"items\"",[51,2210,322],{"class":64},[51,2212,2213],{"class":350},"len",[51,2215,2216],{"class":64},"(cart_items)}\n",[14,2218,2219],{},"This pattern avoids paying the SERIALIZABLE overhead for non-critical reads while providing full serializability guarantees exactly where they matter.",[386,2221,2223],{"id":2222},"using-execution-options-on-scoped-sessions-in-frameworks","Using Execution Options on Scoped Sessions in Frameworks",[14,2225,2226],{},"In FastAPI and similar frameworks, sessions are typically created by a dependency and shared across the request. When you need a non-default isolation level for a specific endpoint, retrieve the underlying connection from the injected session rather than creating a new session:",[42,2228,2230],{"className":44,"code":2229,"language":46,"meta":47,"style":47},"from fastapi import APIRouter, Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy import select\n\nrouter = APIRouter()\n\nasync def get_session() -> AsyncSession:\n    async with AsyncSessionFactory() as session:\n        yield session\n\n@router.post(\"\u002Ftransfer\")\nasync def transfer_endpoint(\n    src_id: int,\n    dst_id: int,\n    amount: int,\n    session: AsyncSession = Depends(get_session),\n) -> dict:\n    # Escalate isolation BEFORE the first statement in this endpoint\n    await session.connection(execution_options={\"isolation_level\": \"SERIALIZABLE\"})\n    async with session.begin():\n        src = await session.get(Account, src_id)\n        dst = await session.get(Account, dst_id)\n        if src.balance \u003C amount:\n            raise ValueError(\"Insufficient balance\")\n        src.balance -= amount\n        dst.balance += amount\n    return {\"status\": \"ok\"}\n",[18,2231,2232,2244,2255,2265,2269,2279,2283,2295,2307,2315,2319,2331,2343,2352,2361,2370,2380,2388,2393,2414,2422,2434,2446,2459,2473,2483,2493],{"__ignoreMap":47},[51,2233,2234,2236,2239,2241],{"class":53,"line":54},[51,2235,155],{"class":68},[51,2237,2238],{"class":64}," fastapi ",[51,2240,161],{"class":68},[51,2242,2243],{"class":64}," APIRouter, Depends\n",[51,2245,2246,2248,2250,2252],{"class":53,"line":61},[51,2247,155],{"class":68},[51,2249,183],{"class":64},[51,2251,161],{"class":68},[51,2253,2254],{"class":64}," AsyncSession\n",[51,2256,2257,2259,2261,2263],{"class":53,"line":75},[51,2258,155],{"class":68},[51,2260,158],{"class":64},[51,2262,161],{"class":68},[51,2264,805],{"class":64},[51,2266,2267],{"class":53,"line":85},[51,2268,113],{"emptyLinePlaceholder":112},[51,2270,2271,2274,2276],{"class":53,"line":103},[51,2272,2273],{"class":64},"router ",[51,2275,69],{"class":68},[51,2277,2278],{"class":64}," APIRouter()\n",[51,2280,2281],{"class":53,"line":109},[51,2282,113],{"emptyLinePlaceholder":112},[51,2284,2285,2287,2289,2292],{"class":53,"line":116},[51,2286,853],{"class":68},[51,2288,856],{"class":68},[51,2290,2291],{"class":610}," get_session",[51,2293,2294],{"class":64},"() -> AsyncSession:\n",[51,2296,2297,2299,2301,2303,2305],{"class":53,"line":131},[51,2298,874],{"class":68},[51,2300,877],{"class":68},[51,2302,880],{"class":64},[51,2304,125],{"class":68},[51,2306,302],{"class":64},[51,2308,2309,2312],{"class":53,"line":218},[51,2310,2311],{"class":68},"        yield",[51,2313,2314],{"class":64}," session\n",[51,2316,2317],{"class":53,"line":224},[51,2318,113],{"emptyLinePlaceholder":112},[51,2320,2321,2324,2326,2329],{"class":53,"line":235},[51,2322,2323],{"class":610},"@router.post",[51,2325,2122],{"class":64},[51,2327,2328],{"class":78},"\"\u002Ftransfer\"",[51,2330,106],{"class":64},[51,2332,2333,2335,2337,2340],{"class":53,"line":256},[51,2334,853],{"class":68},[51,2336,856],{"class":68},[51,2338,2339],{"class":610}," transfer_endpoint",[51,2341,2342],{"class":64},"(\n",[51,2344,2345,2348,2350],{"class":53,"line":265},[51,2346,2347],{"class":64},"    src_id: ",[51,2349,617],{"class":350},[51,2351,82],{"class":64},[51,2353,2354,2357,2359],{"class":53,"line":281},[51,2355,2356],{"class":64},"    dst_id: ",[51,2358,617],{"class":350},[51,2360,82],{"class":64},[51,2362,2363,2366,2368],{"class":53,"line":286},[51,2364,2365],{"class":64},"    amount: ",[51,2367,617],{"class":350},[51,2369,82],{"class":64},[51,2371,2372,2375,2377],{"class":53,"line":292},[51,2373,2374],{"class":64},"    session: AsyncSession ",[51,2376,69],{"class":68},[51,2378,2379],{"class":64}," Depends(get_session),\n",[51,2381,2382,2384,2386],{"class":53,"line":305},[51,2383,620],{"class":64},[51,2385,1411],{"class":350},[51,2387,625],{"class":64},[51,2389,2390],{"class":53,"line":331},[51,2391,2392],{"class":57},"    # Escalate isolation BEFORE the first statement in this endpoint\n",[51,2394,2395,2398,2400,2402,2404,2406,2408,2410,2412],{"class":53,"line":339},[51,2396,2397],{"class":68},"    await",[51,2399,897],{"class":64},[51,2401,311],{"class":88},[51,2403,69],{"class":68},[51,2405,316],{"class":64},[51,2407,319],{"class":78},[51,2409,322],{"class":64},[51,2411,325],{"class":78},[51,2413,328],{"class":64},[51,2415,2416,2418,2420],{"class":53,"line":356},[51,2417,874],{"class":68},[51,2419,877],{"class":68},[51,2421,336],{"class":64},[51,2423,2424,2427,2429,2431],{"class":53,"line":734},[51,2425,2426],{"class":64},"        src ",[51,2428,69],{"class":68},[51,2430,1436],{"class":68},[51,2432,2433],{"class":64}," session.get(Account, src_id)\n",[51,2435,2436,2439,2441,2443],{"class":53,"line":756},[51,2437,2438],{"class":64},"        dst ",[51,2440,69],{"class":68},[51,2442,1436],{"class":68},[51,2444,2445],{"class":64}," session.get(Account, dst_id)\n",[51,2447,2448,2451,2454,2456],{"class":53,"line":765},[51,2449,2450],{"class":68},"        if",[51,2452,2453],{"class":64}," src.balance ",[51,2455,2108],{"class":68},[51,2457,2458],{"class":64}," amount:\n",[51,2460,2461,2464,2466,2468,2471],{"class":53,"line":2142},[51,2462,2463],{"class":68},"            raise",[51,2465,2119],{"class":350},[51,2467,2122],{"class":64},[51,2469,2470],{"class":78},"\"Insufficient balance\"",[51,2472,106],{"class":64},[51,2474,2475,2478,2480],{"class":53,"line":2153},[51,2476,2477],{"class":64},"        src.balance ",[51,2479,362],{"class":68},[51,2481,2482],{"class":64}," amount\n",[51,2484,2485,2488,2491],{"class":53,"line":2182},[51,2486,2487],{"class":64},"        dst.balance ",[51,2489,2490],{"class":68},"+=",[51,2492,2482],{"class":64},[51,2494,2495,2497,2499,2502,2504,2507],{"class":53,"line":2188},[51,2496,2196],{"class":68},[51,2498,2199],{"class":64},[51,2500,2501],{"class":78},"\"status\"",[51,2503,322],{"class":64},[51,2505,2506],{"class":78},"\"ok\"",[51,2508,2058],{"class":64},[14,2510,2511,2512,2514],{},"The dependency yields the session before any statements execute, so ",[18,2513,28],{}," fires before autobegin. The isolation override applies to the entire request transaction without leaking into unrelated endpoints — each request gets a fresh session from the pool.",[37,2516,2518],{"id":2517},"frequently-asked-questions","Frequently Asked Questions",[14,2520,2521,2530,2531,2533,2534,2536,2537,2540,2541,2543,2544,2546],{},[373,2522,2523,2524,2526,2527,2529],{},"Can I set the isolation level using a raw ",[18,2525,1533],{}," SQL statement instead of ",[18,2528,311],{},"?","\nYou can, but it is fragile. SQLAlchemy may issue its own ",[18,2532,379],{}," before your ",[18,2535,1533],{}," fires, causing the database to reject the statement with an ",[18,2538,2539],{},"ActiveSqlTransaction"," error. The ",[18,2542,311],{}," hook is the only safe way because it fires at the DBAPI layer before ",[18,2545,379],{}," is emitted.",[14,2548,2549,2552,2553,1607,2556,2559,2560,2563,2564,2566],{},[373,2550,2551],{},"Does the isolation level override persist across commits within the same connection?","\nNo. After ",[18,2554,2555],{},"COMMIT",[18,2557,2558],{},"ROLLBACK",", the connection is returned to the pool and the pool resets it to the engine-wide default. Each new checkout (each new ",[18,2561,2562],{},"with engine.connect()"," or session context) starts fresh. You must re-apply ",[18,2565,311],{}," on every new connection or session if you need a non-default level.",[14,2568,2569,2572],{},[373,2570,2571],{},"Is there a performance cost to SERIALIZABLE isolation on PostgreSQL?","\nPostgreSQL's Serializable Snapshot Isolation adds CPU and memory overhead to track read\u002Fwrite dependencies — roughly 5–10% on typical OLTP workloads, more on read-heavy analytical queries. The primary cost is serialization failures requiring retries, not locking. Benchmark your specific access pattern before committing SERIALIZABLE engine-wide.",[14,2574,2575,2578,2579,2582,2583,2586,2587,2590],{},[373,2576,2577],{},"How do I verify which isolation level a live connection is actually using?","\nFor PostgreSQL, execute ",[18,2580,2581],{},"SELECT current_setting('transaction_isolation')"," inside the transaction. For MySQL, use ",[18,2584,2585],{},"SELECT @@transaction_isolation",". In tests, you can listen on the ",[18,2588,2589],{},"after_begin"," event to assert the isolation level matches expectations.",[37,2592,2594],{"id":2593},"related","Related",[2596,2597,2598,2605,2612],"ul",{},[2599,2600,2601,2604],"li",{},[31,2602,2603],{"href":33},"Transaction Isolation and Commit Strategies"," — parent guide covering isolation level theory, savepoints, optimistic locking, and retry patterns.",[2599,2606,2607,2611],{},[31,2608,2610],{"href":2609},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002F","Session Lifecycle and Scope Management"," — how session creation and closure affect when isolation overrides must be applied.",[2599,2613,2614,2618,2619,2621,2622,2625],{},[31,2615,2617],{"href":2616},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fcore-vs-orm-architecture-decisions\u002F","Core vs ORM Architecture Decisions"," — when to use a raw ",[18,2620,1577],{}," instead of a ",[18,2623,2624],{},"Session"," for isolation-sensitive operations.",[2627,2628,2629],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":47,"searchDepth":61,"depth":61,"links":2631},[2632,2633,2641,2642,2647,2648],{"id":39,"depth":61,"text":40},{"id":383,"depth":61,"text":384,"children":2634},[2635,2636,2637,2638,2639,2640],{"id":388,"depth":75,"text":389},{"id":417,"depth":75,"text":418},{"id":543,"depth":75,"text":544},{"id":958,"depth":75,"text":959},{"id":1172,"depth":75,"text":1173},{"id":1286,"depth":75,"text":1287},{"id":1476,"depth":61,"text":1477},{"id":1677,"depth":61,"text":1678,"children":2643},[2644,2645,2646],{"id":1681,"depth":75,"text":1682},{"id":1863,"depth":75,"text":1864},{"id":2222,"depth":75,"text":2223},{"id":2517,"depth":61,"text":2518},{"id":2593,"depth":61,"text":2594},"Call connection.execution_options(isolation_level=\"SERIALIZABLE\") before the first statement in a transaction, or pass execution_options={\"isolation_level\": ...} to session.connection() — this guide, part of the transaction isolation and commit strategies reference, shows you exactly where each hook belongs and what breaks when you place it in the wrong order.","md",{"date":2652},"2026-06-18","\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002Fsetting-transaction-isolation-level-per-session",{"title":5,"description":2649},"mastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002Fsetting-transaction-isolation-level-per-session\u002Findex","VYuXYmJoDDXzchT_03H1V8LHFYdBEhjAJPRKaj6DXAg",1781810028985]