[{"data":1,"prerenderedAt":2338},["ShallowReactive",2],{"page-\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Flegacy-1-4-to-2-0-codemod-checklist\u002F":3},{"id":4,"title":5,"body":6,"description":2330,"extension":2331,"meta":2332,"navigation":127,"path":2334,"seo":2335,"stem":2336,"__hash__":2337},"content\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Flegacy-1-4-to-2-0-codemod-checklist\u002Findex.md","Legacy 1.4 to 2.0 Codemod Checklist",{"type":7,"value":8,"toc":2303},"minimark",[9,13,47,56,61,75,81,243,248,378,382,385,396,401,514,524,532,535,570,580,591,604,737,756,766,779,846,852,867,873,1012,1022,1036,1048,1183,1194,1203,1276,1292,1304,1312,1554,1578,1587,1601,1652,1655,1659,1878,1882,1895,1898,2141,2169,2173,2194,2212,2236,2259,2263,2299],[10,11,5],"h1",{"id":12},"legacy-14-to-20-codemod-checklist",[14,15,16,17,21,22,25,26,29,30,29,33,29,36,29,39,42,43,46],"p",{},"Migrate from SQLAlchemy 1.4 to 2.0 by enabling ",[18,19,20],"code",{},"future=True",", resolving all ",[18,23,24],{},"RemovedIn20Warning"," emissions, then systematically replacing ",[18,27,28],{},"session.query()",", ",[18,31,32],{},"Query.get()",[18,34,35],{},"autocommit",[18,37,38],{},"backref",[18,40,41],{},"declarative_base()",", and ",[18,44,45],{},"Column()"," with their 2.0 equivalents before removing the transitional flag.",[14,48,49,50,55],{},"This checklist belongs to the ",[51,52,54],"a",{"href":53},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002F","Migrating Legacy 1.4 Code to 2.0 Syntax"," guide. Work through the steps in the order shown — they build on each other, and skipping ahead will produce confusing errors.",[57,58,60],"h2",{"id":59},"quick-answer","Quick Answer",[14,62,63,64,66,67,70,71,74],{},"The highest-impact single change is replacing ",[18,65,28],{}," with ",[18,68,69],{},"select()"," plus ",[18,72,73],{},"session.execute()",". Every other step is important, but this one touches the most lines of application code.",[14,76,77],{},[78,79,80],"strong",{},"Legacy 1.4 pattern:",[82,83,88],"pre",{"className":84,"code":85,"language":86,"meta":87,"style":87},"language-python shiki shiki-themes github-light github-dark","from sqlalchemy.orm import Session\nfrom myapp.models import User\n\n# Synchronous session.query() — removed in 2.0\ndef get_active_users(session: Session) -> list[User]:\n    return (\n        session.query(User)\n        .filter(User.is_active == True)\n        .order_by(User.created_at.desc())\n        .all()\n    )\n\n# Query.get() — also removed in 2.0\ndef get_user_by_pk(session: Session, user_id: int) -> User | None:\n    return session.query(User).get(user_id)\n","python","",[18,89,90,109,122,129,136,149,158,164,180,186,192,198,203,209,235],{"__ignoreMap":87},[91,92,95,99,103,106],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"szBVR","from",[91,100,102],{"class":101},"sVt8B"," sqlalchemy.orm ",[91,104,105],{"class":97},"import",[91,107,108],{"class":101}," Session\n",[91,110,112,114,117,119],{"class":93,"line":111},2,[91,113,98],{"class":97},[91,115,116],{"class":101}," myapp.models ",[91,118,105],{"class":97},[91,120,121],{"class":101}," User\n",[91,123,125],{"class":93,"line":124},3,[91,126,128],{"emptyLinePlaceholder":127},true,"\n",[91,130,132],{"class":93,"line":131},4,[91,133,135],{"class":134},"sJ8bj","# Synchronous session.query() — removed in 2.0\n",[91,137,139,142,146],{"class":93,"line":138},5,[91,140,141],{"class":97},"def",[91,143,145],{"class":144},"sScJk"," get_active_users",[91,147,148],{"class":101},"(session: Session) -> list[User]:\n",[91,150,152,155],{"class":93,"line":151},6,[91,153,154],{"class":97},"    return",[91,156,157],{"class":101}," (\n",[91,159,161],{"class":93,"line":160},7,[91,162,163],{"class":101},"        session.query(User)\n",[91,165,167,170,173,177],{"class":93,"line":166},8,[91,168,169],{"class":101},"        .filter(User.is_active ",[91,171,172],{"class":97},"==",[91,174,176],{"class":175},"sj4cs"," True",[91,178,179],{"class":101},")\n",[91,181,183],{"class":93,"line":182},9,[91,184,185],{"class":101},"        .order_by(User.created_at.desc())\n",[91,187,189],{"class":93,"line":188},10,[91,190,191],{"class":101},"        .all()\n",[91,193,195],{"class":93,"line":194},11,[91,196,197],{"class":101},"    )\n",[91,199,201],{"class":93,"line":200},12,[91,202,128],{"emptyLinePlaceholder":127},[91,204,206],{"class":93,"line":205},13,[91,207,208],{"class":134},"# Query.get() — also removed in 2.0\n",[91,210,212,214,217,220,223,226,229,232],{"class":93,"line":211},14,[91,213,141],{"class":97},[91,215,216],{"class":144}," get_user_by_pk",[91,218,219],{"class":101},"(session: Session, user_id: ",[91,221,222],{"class":175},"int",[91,224,225],{"class":101},") -> User ",[91,227,228],{"class":97},"|",[91,230,231],{"class":175}," None",[91,233,234],{"class":101},":\n",[91,236,238,240],{"class":93,"line":237},15,[91,239,154],{"class":97},[91,241,242],{"class":101}," session.query(User).get(user_id)\n",[14,244,245],{},[78,246,247],{},"Modern 2.0 pattern:",[82,249,251],{"className":84,"code":250,"language":86,"meta":87,"style":87},"from sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom myapp.models import User\n\n# select() + session.execute() — the 2.0 way\ndef get_active_users(session: Session) -> list[User]:\n    stmt = (\n        select(User)\n        .where(User.is_active == True)\n        .order_by(User.created_at.desc())\n    )\n    return session.execute(stmt).scalars().all()\n\n# Session.get() replaces Query.get()\ndef get_user_by_pk(session: Session, user_id: int) -> User | None:\n    return session.get(User, user_id)\n",[18,252,253,265,275,285,289,294,302,312,317,328,332,336,343,347,352,370],{"__ignoreMap":87},[91,254,255,257,260,262],{"class":93,"line":94},[91,256,98],{"class":97},[91,258,259],{"class":101}," sqlalchemy ",[91,261,105],{"class":97},[91,263,264],{"class":101}," select\n",[91,266,267,269,271,273],{"class":93,"line":111},[91,268,98],{"class":97},[91,270,102],{"class":101},[91,272,105],{"class":97},[91,274,108],{"class":101},[91,276,277,279,281,283],{"class":93,"line":124},[91,278,98],{"class":97},[91,280,116],{"class":101},[91,282,105],{"class":97},[91,284,121],{"class":101},[91,286,287],{"class":93,"line":131},[91,288,128],{"emptyLinePlaceholder":127},[91,290,291],{"class":93,"line":138},[91,292,293],{"class":134},"# select() + session.execute() — the 2.0 way\n",[91,295,296,298,300],{"class":93,"line":151},[91,297,141],{"class":97},[91,299,145],{"class":144},[91,301,148],{"class":101},[91,303,304,307,310],{"class":93,"line":160},[91,305,306],{"class":101},"    stmt ",[91,308,309],{"class":97},"=",[91,311,157],{"class":101},[91,313,314],{"class":93,"line":166},[91,315,316],{"class":101},"        select(User)\n",[91,318,319,322,324,326],{"class":93,"line":182},[91,320,321],{"class":101},"        .where(User.is_active ",[91,323,172],{"class":97},[91,325,176],{"class":175},[91,327,179],{"class":101},[91,329,330],{"class":93,"line":188},[91,331,185],{"class":101},[91,333,334],{"class":93,"line":194},[91,335,197],{"class":101},[91,337,338,340],{"class":93,"line":200},[91,339,154],{"class":97},[91,341,342],{"class":101}," session.execute(stmt).scalars().all()\n",[91,344,345],{"class":93,"line":205},[91,346,128],{"emptyLinePlaceholder":127},[91,348,349],{"class":93,"line":211},[91,350,351],{"class":134},"# Session.get() replaces Query.get()\n",[91,353,354,356,358,360,362,364,366,368],{"class":93,"line":237},[91,355,141],{"class":97},[91,357,216],{"class":144},[91,359,219],{"class":101},[91,361,222],{"class":175},[91,363,225],{"class":101},[91,365,228],{"class":97},[91,367,231],{"class":175},[91,369,234],{"class":101},[91,371,373,375],{"class":93,"line":372},16,[91,374,154],{"class":97},[91,376,377],{"class":101}," session.get(User, user_id)\n",[57,379,381],{"id":380},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[14,383,384],{},"Every codemod step has a direct consequence for async code. Working through them in order ensures you do not introduce async-incompatible patterns while you are mid-migration.",[386,387,389,390,392,393],"h3",{"id":388},"step-1-enable-futuretrue-on-create_engine","Step 1 — Enable ",[18,391,20],{}," on ",[18,394,395],{},"create_engine()",[14,397,398,400],{},[18,399,20],{}," activates 2.0-style behavior inside a 1.4 install. This is the migration bridge.",[82,402,404],{"className":84,"code":403,"language":86,"meta":87,"style":87},"from sqlalchemy import create_engine\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\n# Synchronous engine — add future=True\nengine = create_engine(\n    \"postgresql+psycopg2:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    future=True,\n)\n\n# Async engine — already implies future=True, but explicit is fine\nasync_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    future=True,\n)\n",[18,405,406,417,429,433,438,448,457,470,474,478,483,493,500,510],{"__ignoreMap":87},[91,407,408,410,412,414],{"class":93,"line":94},[91,409,98],{"class":97},[91,411,259],{"class":101},[91,413,105],{"class":97},[91,415,416],{"class":101}," create_engine\n",[91,418,419,421,424,426],{"class":93,"line":111},[91,420,98],{"class":97},[91,422,423],{"class":101}," sqlalchemy.ext.asyncio ",[91,425,105],{"class":97},[91,427,428],{"class":101}," create_async_engine\n",[91,430,431],{"class":93,"line":124},[91,432,128],{"emptyLinePlaceholder":127},[91,434,435],{"class":93,"line":131},[91,436,437],{"class":134},"# Synchronous engine — add future=True\n",[91,439,440,443,445],{"class":93,"line":138},[91,441,442],{"class":101},"engine ",[91,444,309],{"class":97},[91,446,447],{"class":101}," create_engine(\n",[91,449,450,454],{"class":93,"line":151},[91,451,453],{"class":452},"sZZnC","    \"postgresql+psycopg2:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[91,455,456],{"class":101},",\n",[91,458,459,463,465,468],{"class":93,"line":160},[91,460,462],{"class":461},"s4XuR","    future",[91,464,309],{"class":97},[91,466,467],{"class":175},"True",[91,469,456],{"class":101},[91,471,472],{"class":93,"line":166},[91,473,179],{"class":101},[91,475,476],{"class":93,"line":182},[91,477,128],{"emptyLinePlaceholder":127},[91,479,480],{"class":93,"line":188},[91,481,482],{"class":134},"# Async engine — already implies future=True, but explicit is fine\n",[91,484,485,488,490],{"class":93,"line":194},[91,486,487],{"class":101},"async_engine ",[91,489,309],{"class":97},[91,491,492],{"class":101}," create_async_engine(\n",[91,494,495,498],{"class":93,"line":200},[91,496,497],{"class":452},"    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[91,499,456],{"class":101},[91,501,502,504,506,508],{"class":93,"line":205},[91,503,462],{"class":461},[91,505,309],{"class":97},[91,507,467],{"class":175},[91,509,456],{"class":101},[91,511,512],{"class":93,"line":211},[91,513,179],{"class":101},[14,515,516,517,519,520,523],{},"With ",[18,518,20],{}," set, the engine enforces 2.0 connection semantics: autobegin, no autocommit, and Core text queries require ",[18,521,522],{},"text()",". This is the minimum change that makes subsequent steps testable.",[386,525,527,528,531],{"id":526},"step-2-set-sqlalchemy_warn_201-and-audit-all-warnings","Step 2 — Set ",[18,529,530],{},"SQLALCHEMY_WARN_20=1"," and audit all warnings",[14,533,534],{},"Before you change any model or query code, turn on every deprecation warning. Run your test suite with the environment variable set and capture the output.",[82,536,540],{"className":537,"code":538,"language":539,"meta":87,"style":87},"language-bash shiki shiki-themes github-light github-dark","SQLALCHEMY_WARN_20=1 python -W error::sqlalchemy.exc.RemovedIn20Warning -m pytest tests\u002F\n","bash",[18,541,542],{"__ignoreMap":87},[91,543,544,547,549,552,555,558,561,564,567],{"class":93,"line":94},[91,545,546],{"class":101},"SQLALCHEMY_WARN_20",[91,548,309],{"class":97},[91,550,551],{"class":452},"1",[91,553,554],{"class":144}," python",[91,556,557],{"class":175}," -W",[91,559,560],{"class":452}," error::sqlalchemy.exc.RemovedIn20Warning",[91,562,563],{"class":175}," -m",[91,565,566],{"class":452}," pytest",[91,568,569],{"class":452}," tests\u002F\n",[14,571,572,573,576,577,579],{},"The ",[18,574,575],{},"-W error"," flag promotes every ",[18,578,24],{}," to a hard exception, making tests fail at the exact call site instead of printing a warning you can ignore. Fix warnings in the order your test output shows them — that order usually tracks call frequency, so the busiest code paths surface first.",[386,581,583,584,66,586,588,589],{"id":582},"step-3-replace-sessionquery-with-select-sessionexecute","Step 3 — Replace ",[18,585,28],{},[18,587,69],{}," + ",[18,590,73],{},[14,592,593,594,597,598,600,601,603],{},"This is the largest mechanical change. The async implication is significant: ",[18,595,596],{},"AsyncSession"," never supported ",[18,599,28],{}," at all, so any code that ran synchronously via ",[18,602,28],{}," cannot be directly ported to async without this step.",[82,605,607],{"className":84,"code":606,"language":86,"meta":87,"style":87},"from sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom myapp.models import Order, User\n\nasync def get_open_orders_for_user(\n    session: AsyncSession,\n    user_id: int,\n) -> list[Order]:\n    stmt = (\n        select(Order)\n        .where(Order.user_id == user_id, Order.status == \"open\")\n        .order_by(Order.created_at.desc())\n    )\n    result = await session.execute(stmt)\n    return result.scalars().all()\n",[18,608,609,619,630,641,645,659,664,673,678,686,691,708,713,717,730],{"__ignoreMap":87},[91,610,611,613,615,617],{"class":93,"line":94},[91,612,98],{"class":97},[91,614,259],{"class":101},[91,616,105],{"class":97},[91,618,264],{"class":101},[91,620,621,623,625,627],{"class":93,"line":111},[91,622,98],{"class":97},[91,624,423],{"class":101},[91,626,105],{"class":97},[91,628,629],{"class":101}," AsyncSession\n",[91,631,632,634,636,638],{"class":93,"line":124},[91,633,98],{"class":97},[91,635,116],{"class":101},[91,637,105],{"class":97},[91,639,640],{"class":101}," Order, User\n",[91,642,643],{"class":93,"line":131},[91,644,128],{"emptyLinePlaceholder":127},[91,646,647,650,653,656],{"class":93,"line":138},[91,648,649],{"class":97},"async",[91,651,652],{"class":97}," def",[91,654,655],{"class":144}," get_open_orders_for_user",[91,657,658],{"class":101},"(\n",[91,660,661],{"class":93,"line":151},[91,662,663],{"class":101},"    session: AsyncSession,\n",[91,665,666,669,671],{"class":93,"line":160},[91,667,668],{"class":101},"    user_id: ",[91,670,222],{"class":175},[91,672,456],{"class":101},[91,674,675],{"class":93,"line":166},[91,676,677],{"class":101},") -> list[Order]:\n",[91,679,680,682,684],{"class":93,"line":182},[91,681,306],{"class":101},[91,683,309],{"class":97},[91,685,157],{"class":101},[91,687,688],{"class":93,"line":188},[91,689,690],{"class":101},"        select(Order)\n",[91,692,693,696,698,701,703,706],{"class":93,"line":194},[91,694,695],{"class":101},"        .where(Order.user_id ",[91,697,172],{"class":97},[91,699,700],{"class":101}," user_id, Order.status ",[91,702,172],{"class":97},[91,704,705],{"class":452}," \"open\"",[91,707,179],{"class":101},[91,709,710],{"class":93,"line":200},[91,711,712],{"class":101},"        .order_by(Order.created_at.desc())\n",[91,714,715],{"class":93,"line":205},[91,716,197],{"class":101},[91,718,719,722,724,727],{"class":93,"line":211},[91,720,721],{"class":101},"    result ",[91,723,309],{"class":97},[91,725,726],{"class":97}," await",[91,728,729],{"class":101}," session.execute(stmt)\n",[91,731,732,734],{"class":93,"line":237},[91,733,154],{"class":97},[91,735,736],{"class":101}," result.scalars().all()\n",[14,738,572,739,742,743,745,746,749,750,752,753,755],{},[18,740,741],{},".scalars()"," call is required whenever you select a single ORM entity. Without it, ",[18,744,73],{}," returns ",[18,747,748],{},"Row"," objects rather than mapped instances. For multi-entity queries, drop ",[18,751,741],{}," and unpack the ",[18,754,748],{}," tuples directly.",[386,757,759,760,66,763],{"id":758},"step-4-replace-querygetpk-with-sessiongetmodel-pk","Step 4 — Replace ",[18,761,762],{},"Query.get(pk)",[18,764,765],{},"Session.get(Model, pk)",[14,767,768,771,772,775,776,778],{},[18,769,770],{},"Session.get()"," and ",[18,773,774],{},"AsyncSession.get()"," hit the identity map first, then issue a SELECT only on a cache miss — the same semantics as ",[18,777,32],{},".",[82,780,782],{"className":84,"code":781,"language":86,"meta":87,"style":87},"from sqlalchemy.ext.asyncio import AsyncSession\nfrom myapp.models import Invoice\n\nasync def fetch_invoice(session: AsyncSession, invoice_id: int) -> Invoice | None:\n    # Session.get() works identically in sync and async contexts\n    return await session.get(Invoice, invoice_id)\n",[18,783,784,794,805,809,832,837],{"__ignoreMap":87},[91,785,786,788,790,792],{"class":93,"line":94},[91,787,98],{"class":97},[91,789,423],{"class":101},[91,791,105],{"class":97},[91,793,629],{"class":101},[91,795,796,798,800,802],{"class":93,"line":111},[91,797,98],{"class":97},[91,799,116],{"class":101},[91,801,105],{"class":97},[91,803,804],{"class":101}," Invoice\n",[91,806,807],{"class":93,"line":124},[91,808,128],{"emptyLinePlaceholder":127},[91,810,811,813,815,818,821,823,826,828,830],{"class":93,"line":131},[91,812,649],{"class":97},[91,814,652],{"class":97},[91,816,817],{"class":144}," fetch_invoice",[91,819,820],{"class":101},"(session: AsyncSession, invoice_id: ",[91,822,222],{"class":175},[91,824,825],{"class":101},") -> Invoice ",[91,827,228],{"class":97},[91,829,231],{"class":175},[91,831,234],{"class":101},[91,833,834],{"class":93,"line":138},[91,835,836],{"class":134},"    # Session.get() works identically in sync and async contexts\n",[91,838,839,841,843],{"class":93,"line":151},[91,840,154],{"class":97},[91,842,726],{"class":97},[91,844,845],{"class":101}," session.get(Invoice, invoice_id)\n",[14,847,848,849,778],{},"Composite primary keys are supported as a tuple: ",[18,850,851],{},"session.get(Order, (tenant_id, order_id))",[386,853,855,856,859,860,863,864],{"id":854},"step-5-remove-autocommittrue-switch-to-explicit-sessionbegin-commit","Step 5 — Remove ",[18,857,858],{},"autocommit=True"," — switch to explicit ",[18,861,862],{},"session.begin()"," \u002F ",[18,865,866],{},"commit()",[14,868,869,870,872],{},"SQLAlchemy 2.0 removed the ",[18,871,35],{}," Session flag. The replacement is explicit transaction management. In async code this matters especially because you often scope a session to a single request or task.",[82,874,876],{"className":84,"code":875,"language":86,"meta":87,"style":87},"from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\")\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nasync def create_tenant(name: str) -> None:\n    async with AsyncSessionLocal() as session:\n        async with session.begin():          # explicit transaction\n            tenant = Tenant(name=name)\n            session.add(tenant)\n        # commit fires automatically when the inner context exits cleanly\n",[18,877,878,889,893,907,927,931,954,971,984,1002,1007],{"__ignoreMap":87},[91,879,880,882,884,886],{"class":93,"line":94},[91,881,98],{"class":97},[91,883,423],{"class":101},[91,885,105],{"class":97},[91,887,888],{"class":101}," AsyncSession, async_sessionmaker, create_async_engine\n",[91,890,891],{"class":93,"line":111},[91,892,128],{"emptyLinePlaceholder":127},[91,894,895,897,899,902,905],{"class":93,"line":124},[91,896,442],{"class":101},[91,898,309],{"class":97},[91,900,901],{"class":101}," create_async_engine(",[91,903,904],{"class":452},"\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[91,906,179],{"class":101},[91,908,909,912,914,917,920,922,925],{"class":93,"line":131},[91,910,911],{"class":101},"AsyncSessionLocal ",[91,913,309],{"class":97},[91,915,916],{"class":101}," async_sessionmaker(engine, ",[91,918,919],{"class":461},"expire_on_commit",[91,921,309],{"class":97},[91,923,924],{"class":175},"False",[91,926,179],{"class":101},[91,928,929],{"class":93,"line":138},[91,930,128],{"emptyLinePlaceholder":127},[91,932,933,935,937,940,943,946,949,952],{"class":93,"line":151},[91,934,649],{"class":97},[91,936,652],{"class":97},[91,938,939],{"class":144}," create_tenant",[91,941,942],{"class":101},"(name: ",[91,944,945],{"class":175},"str",[91,947,948],{"class":101},") -> ",[91,950,951],{"class":175},"None",[91,953,234],{"class":101},[91,955,956,959,962,965,968],{"class":93,"line":160},[91,957,958],{"class":97},"    async",[91,960,961],{"class":97}," with",[91,963,964],{"class":101}," AsyncSessionLocal() ",[91,966,967],{"class":97},"as",[91,969,970],{"class":101}," session:\n",[91,972,973,976,978,981],{"class":93,"line":166},[91,974,975],{"class":97},"        async",[91,977,961],{"class":97},[91,979,980],{"class":101}," session.begin():          ",[91,982,983],{"class":134},"# explicit transaction\n",[91,985,986,989,991,994,997,999],{"class":93,"line":182},[91,987,988],{"class":101},"            tenant ",[91,990,309],{"class":97},[91,992,993],{"class":101}," Tenant(",[91,995,996],{"class":461},"name",[91,998,309],{"class":97},[91,1000,1001],{"class":101},"name)\n",[91,1003,1004],{"class":93,"line":188},[91,1005,1006],{"class":101},"            session.add(tenant)\n",[91,1008,1009],{"class":93,"line":194},[91,1010,1011],{"class":134},"        # commit fires automatically when the inner context exits cleanly\n",[14,1013,572,1014,1017,1018,778],{},[18,1015,1016],{},"async with session.begin()"," context manager commits on success and rolls back on exception. Never rely on implicit commit in 2.0. For more on commit strategy patterns, see ",[51,1019,1021],{"href":1020},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Ftransaction-isolation-and-commit-strategies\u002F","Transaction Isolation and Commit Strategies",[386,1023,1025,1026,1029,1030,1033,1034],{"id":1024},"step-6-update-relationship-to-use-back_populates-instead-of-backref","Step 6 — Update ",[18,1027,1028],{},"relationship()"," to use ",[18,1031,1032],{},"back_populates"," instead of ",[18,1035,38],{},[14,1037,1038,1040,1041,1043,1044,1047],{},[18,1039,38],{}," still works in 2.0 but is considered a legacy shortcut. ",[18,1042,1032],{}," makes both sides of the relationship explicit, which is required when you add ",[18,1045,1046],{},"Mapped[T]"," annotations in the next step.",[82,1049,1051],{"className":84,"code":1050,"language":86,"meta":87,"style":87},"# Legacy — backref implicitly creates the reverse attribute\nclass User(Base):\n    orders = relationship(\"Order\", backref=\"user\")\n\n# 2.0 — both sides declared, compatible with Mapped[T]\nclass User(Base):\n    orders: Mapped[list[\"Order\"]] = relationship(back_populates=\"user\")\n\nclass Order(Base):\n    user: Mapped[\"User\"] = relationship(back_populates=\"orders\")\n",[18,1052,1053,1058,1075,1099,1103,1108,1120,1142,1146,1159],{"__ignoreMap":87},[91,1054,1055],{"class":93,"line":94},[91,1056,1057],{"class":134},"# Legacy — backref implicitly creates the reverse attribute\n",[91,1059,1060,1063,1066,1069,1072],{"class":93,"line":111},[91,1061,1062],{"class":97},"class",[91,1064,1065],{"class":144}," User",[91,1067,1068],{"class":101},"(",[91,1070,1071],{"class":144},"Base",[91,1073,1074],{"class":101},"):\n",[91,1076,1077,1080,1082,1085,1088,1090,1092,1094,1097],{"class":93,"line":124},[91,1078,1079],{"class":101},"    orders ",[91,1081,309],{"class":97},[91,1083,1084],{"class":101}," relationship(",[91,1086,1087],{"class":452},"\"Order\"",[91,1089,29],{"class":101},[91,1091,38],{"class":461},[91,1093,309],{"class":97},[91,1095,1096],{"class":452},"\"user\"",[91,1098,179],{"class":101},[91,1100,1101],{"class":93,"line":131},[91,1102,128],{"emptyLinePlaceholder":127},[91,1104,1105],{"class":93,"line":138},[91,1106,1107],{"class":134},"# 2.0 — both sides declared, compatible with Mapped[T]\n",[91,1109,1110,1112,1114,1116,1118],{"class":93,"line":151},[91,1111,1062],{"class":97},[91,1113,1065],{"class":144},[91,1115,1068],{"class":101},[91,1117,1071],{"class":144},[91,1119,1074],{"class":101},[91,1121,1122,1125,1127,1130,1132,1134,1136,1138,1140],{"class":93,"line":160},[91,1123,1124],{"class":101},"    orders: Mapped[list[",[91,1126,1087],{"class":452},[91,1128,1129],{"class":101},"]] ",[91,1131,309],{"class":97},[91,1133,1084],{"class":101},[91,1135,1032],{"class":461},[91,1137,309],{"class":97},[91,1139,1096],{"class":452},[91,1141,179],{"class":101},[91,1143,1144],{"class":93,"line":166},[91,1145,128],{"emptyLinePlaceholder":127},[91,1147,1148,1150,1153,1155,1157],{"class":93,"line":182},[91,1149,1062],{"class":97},[91,1151,1152],{"class":144}," Order",[91,1154,1068],{"class":101},[91,1156,1071],{"class":144},[91,1158,1074],{"class":101},[91,1160,1161,1164,1167,1170,1172,1174,1176,1178,1181],{"class":93,"line":188},[91,1162,1163],{"class":101},"    user: Mapped[",[91,1165,1166],{"class":452},"\"User\"",[91,1168,1169],{"class":101},"] ",[91,1171,309],{"class":97},[91,1173,1084],{"class":101},[91,1175,1032],{"class":461},[91,1177,309],{"class":97},[91,1179,1180],{"class":452},"\"orders\"",[91,1182,179],{"class":101},[386,1184,1186,1187,1189,1190,1193],{"id":1185},"step-7-replace-declarative_base-with-a-declarativebase-subclass","Step 7 — Replace ",[18,1188,41],{}," with a ",[18,1191,1192],{},"DeclarativeBase"," subclass",[14,1195,1196,1197,1199,1200,1202],{},"The module-level ",[18,1198,41],{}," factory is replaced by subclassing ",[18,1201,1192],{},". This unlocks PEP 681 dataclass integration and proper type inference.",[82,1204,1206],{"className":84,"code":1205,"language":86,"meta":87,"style":87},"# Legacy\nfrom sqlalchemy.orm import declarative_base\nBase = declarative_base()\n\n# 2.0\nfrom sqlalchemy.orm import DeclarativeBase\n\nclass Base(DeclarativeBase):\n    pass\n",[18,1207,1208,1213,1224,1234,1238,1243,1254,1258,1271],{"__ignoreMap":87},[91,1209,1210],{"class":93,"line":94},[91,1211,1212],{"class":134},"# Legacy\n",[91,1214,1215,1217,1219,1221],{"class":93,"line":111},[91,1216,98],{"class":97},[91,1218,102],{"class":101},[91,1220,105],{"class":97},[91,1222,1223],{"class":101}," declarative_base\n",[91,1225,1226,1229,1231],{"class":93,"line":124},[91,1227,1228],{"class":101},"Base ",[91,1230,309],{"class":97},[91,1232,1233],{"class":101}," declarative_base()\n",[91,1235,1236],{"class":93,"line":131},[91,1237,128],{"emptyLinePlaceholder":127},[91,1239,1240],{"class":93,"line":138},[91,1241,1242],{"class":134},"# 2.0\n",[91,1244,1245,1247,1249,1251],{"class":93,"line":151},[91,1246,98],{"class":97},[91,1248,102],{"class":101},[91,1250,105],{"class":97},[91,1252,1253],{"class":101}," DeclarativeBase\n",[91,1255,1256],{"class":93,"line":160},[91,1257,128],{"emptyLinePlaceholder":127},[91,1259,1260,1262,1265,1267,1269],{"class":93,"line":166},[91,1261,1062],{"class":97},[91,1263,1264],{"class":144}," Base",[91,1266,1068],{"class":101},[91,1268,1192],{"class":144},[91,1270,1074],{"class":101},[91,1272,1273],{"class":93,"line":182},[91,1274,1275],{"class":97},"    pass\n",[14,1277,1278,1279,1281,1282,771,1285,1288,1289,1291],{},"Your existing model classes continue to inherit from ",[18,1280,1071],{}," without any other changes. The ",[18,1283,1284],{},"metadata",[18,1286,1287],{},"registry"," attributes are now class-level attributes on your ",[18,1290,1071],{}," subclass rather than module globals.",[386,1293,1295,1296,66,1298,588,1301,1303],{"id":1294},"step-8-replace-column-with-mapped_column-mappedt-annotations","Step 8 — Replace ",[18,1297,45],{},[18,1299,1300],{},"mapped_column()",[18,1302,1046],{}," annotations",[14,1305,1306,1307,1311],{},"This step is the deepest change and the one that delivers the most long-term value: full static type-checking for ORM models. See the ",[51,1308,1310],{"href":1309},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Fstep-by-step-guide-to-sqlalchemy-20-type-annotations\u002F","Step-by-Step Guide to SQLAlchemy 2.0 Type Annotations"," for the full annotation migration walkthrough.",[82,1313,1315],{"className":84,"code":1314,"language":86,"meta":87,"style":87},"from __future__ import annotations\n\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom sqlalchemy import Numeric, String, func\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Product(Base):\n    __tablename__ = \"products\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str] = mapped_column(String(255), nullable=False)\n    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))\n    created_at: Mapped[datetime] = mapped_column(server_default=func.now())\n    description: Mapped[str | None] = mapped_column(String(2000))\n",[18,1316,1317,1330,1334,1346,1358,1362,1373,1384,1388,1400,1404,1408,1421,1431,1435,1461,1490,1512,1530],{"__ignoreMap":87},[91,1318,1319,1321,1324,1327],{"class":93,"line":94},[91,1320,98],{"class":97},[91,1322,1323],{"class":175}," __future__",[91,1325,1326],{"class":97}," import",[91,1328,1329],{"class":101}," annotations\n",[91,1331,1332],{"class":93,"line":111},[91,1333,128],{"emptyLinePlaceholder":127},[91,1335,1336,1338,1341,1343],{"class":93,"line":124},[91,1337,98],{"class":97},[91,1339,1340],{"class":101}," datetime ",[91,1342,105],{"class":97},[91,1344,1345],{"class":101}," datetime\n",[91,1347,1348,1350,1353,1355],{"class":93,"line":131},[91,1349,98],{"class":97},[91,1351,1352],{"class":101}," decimal ",[91,1354,105],{"class":97},[91,1356,1357],{"class":101}," Decimal\n",[91,1359,1360],{"class":93,"line":138},[91,1361,128],{"emptyLinePlaceholder":127},[91,1363,1364,1366,1368,1370],{"class":93,"line":151},[91,1365,98],{"class":97},[91,1367,259],{"class":101},[91,1369,105],{"class":97},[91,1371,1372],{"class":101}," Numeric, String, func\n",[91,1374,1375,1377,1379,1381],{"class":93,"line":160},[91,1376,98],{"class":97},[91,1378,102],{"class":101},[91,1380,105],{"class":97},[91,1382,1383],{"class":101}," DeclarativeBase, Mapped, mapped_column\n",[91,1385,1386],{"class":93,"line":166},[91,1387,128],{"emptyLinePlaceholder":127},[91,1389,1390,1392,1394,1396,1398],{"class":93,"line":182},[91,1391,1062],{"class":97},[91,1393,1264],{"class":144},[91,1395,1068],{"class":101},[91,1397,1192],{"class":144},[91,1399,1074],{"class":101},[91,1401,1402],{"class":93,"line":188},[91,1403,1275],{"class":97},[91,1405,1406],{"class":93,"line":194},[91,1407,128],{"emptyLinePlaceholder":127},[91,1409,1410,1412,1415,1417,1419],{"class":93,"line":200},[91,1411,1062],{"class":97},[91,1413,1414],{"class":144}," Product",[91,1416,1068],{"class":101},[91,1418,1071],{"class":144},[91,1420,1074],{"class":101},[91,1422,1423,1426,1428],{"class":93,"line":205},[91,1424,1425],{"class":101},"    __tablename__ ",[91,1427,309],{"class":97},[91,1429,1430],{"class":452}," \"products\"\n",[91,1432,1433],{"class":93,"line":211},[91,1434,128],{"emptyLinePlaceholder":127},[91,1436,1437,1440,1443,1445,1447,1449,1452,1455,1457,1459],{"class":93,"line":237},[91,1438,1439],{"class":175},"    id",[91,1441,1442],{"class":101},": Mapped[",[91,1444,222],{"class":175},[91,1446,1169],{"class":101},[91,1448,309],{"class":97},[91,1450,1451],{"class":101}," mapped_column(",[91,1453,1454],{"class":461},"primary_key",[91,1456,309],{"class":97},[91,1458,467],{"class":175},[91,1460,179],{"class":101},[91,1462,1463,1466,1468,1470,1472,1475,1478,1481,1484,1486,1488],{"class":93,"line":372},[91,1464,1465],{"class":101},"    name: Mapped[",[91,1467,945],{"class":175},[91,1469,1169],{"class":101},[91,1471,309],{"class":97},[91,1473,1474],{"class":101}," mapped_column(String(",[91,1476,1477],{"class":175},"255",[91,1479,1480],{"class":101},"), ",[91,1482,1483],{"class":461},"nullable",[91,1485,309],{"class":97},[91,1487,924],{"class":175},[91,1489,179],{"class":101},[91,1491,1493,1496,1498,1501,1504,1506,1509],{"class":93,"line":1492},17,[91,1494,1495],{"class":101},"    price: Mapped[Decimal] ",[91,1497,309],{"class":97},[91,1499,1500],{"class":101}," mapped_column(Numeric(",[91,1502,1503],{"class":175},"10",[91,1505,29],{"class":101},[91,1507,1508],{"class":175},"2",[91,1510,1511],{"class":101},"))\n",[91,1513,1515,1518,1520,1522,1525,1527],{"class":93,"line":1514},18,[91,1516,1517],{"class":101},"    created_at: Mapped[datetime] ",[91,1519,309],{"class":97},[91,1521,1451],{"class":101},[91,1523,1524],{"class":461},"server_default",[91,1526,309],{"class":97},[91,1528,1529],{"class":101},"func.now())\n",[91,1531,1533,1536,1538,1541,1543,1545,1547,1549,1552],{"class":93,"line":1532},19,[91,1534,1535],{"class":101},"    description: Mapped[",[91,1537,945],{"class":175},[91,1539,1540],{"class":97}," |",[91,1542,231],{"class":175},[91,1544,1169],{"class":101},[91,1546,309],{"class":97},[91,1548,1474],{"class":101},[91,1550,1551],{"class":175},"2000",[91,1553,1511],{"class":101},[14,1555,572,1556,1558,1559,1562,1563,1566,1567,1562,1570,1573,1574,1577],{},[18,1557,1046],{}," annotation drives nullability: ",[18,1560,1561],{},"Mapped[str]"," implies ",[18,1564,1565],{},"NOT NULL",", while ",[18,1568,1569],{},"Mapped[str | None]",[18,1571,1572],{},"nullable=True",". You can drop explicit ",[18,1575,1576],{},"nullable="," arguments once annotations are in place.",[386,1579,1581,1582,1584,1585],{"id":1580},"step-9-remove-futuretrue-and-unset-sqlalchemy_warn_20","Step 9 — Remove ",[18,1583,20],{}," and unset ",[18,1586,546],{},[14,1588,1589,1590,1592,1593,1595,1596,1598,1599,778],{},"When your test suite passes with zero ",[18,1591,24],{}," emissions and all model code uses ",[18,1594,1046],{}," annotations, remove the transitional ",[18,1597,20],{}," argument (it is the default and has no effect in 2.0) and stop setting ",[18,1600,546],{},[82,1602,1604],{"className":84,"code":1603,"language":86,"meta":87,"style":87},"# Clean 2.0 engine — no future= needed\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    pool_size=10,\n    max_overflow=20,\n)\n",[18,1605,1606,1611,1619,1625,1636,1648],{"__ignoreMap":87},[91,1607,1608],{"class":93,"line":94},[91,1609,1610],{"class":134},"# Clean 2.0 engine — no future= needed\n",[91,1612,1613,1615,1617],{"class":93,"line":111},[91,1614,442],{"class":101},[91,1616,309],{"class":97},[91,1618,492],{"class":101},[91,1620,1621,1623],{"class":93,"line":124},[91,1622,497],{"class":452},[91,1624,456],{"class":101},[91,1626,1627,1630,1632,1634],{"class":93,"line":131},[91,1628,1629],{"class":461},"    pool_size",[91,1631,309],{"class":97},[91,1633,1503],{"class":175},[91,1635,456],{"class":101},[91,1637,1638,1641,1643,1646],{"class":93,"line":138},[91,1639,1640],{"class":461},"    max_overflow",[91,1642,309],{"class":97},[91,1644,1645],{"class":175},"20",[91,1647,456],{"class":101},[91,1649,1650],{"class":93,"line":151},[91,1651,179],{"class":101},[14,1653,1654],{},"At this point your codebase is fully 2.0-native. You can now safely upgrade from SQLAlchemy 1.4 to the latest 2.x release.",[57,1656,1658],{"id":1657},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1660,1661,1662,1678],"table",{},[1663,1664,1665],"thead",{},[1666,1667,1668,1672,1675],"tr",{},[1669,1670,1671],"th",{},"Exact warning \u002F error string",[1669,1673,1674],{},"Root cause",[1669,1676,1677],{},"Production fix",[1679,1680,1681,1702,1724,1743,1767,1788,1813,1831,1849],"tbody",{},[1666,1682,1683,1689,1696],{},[1684,1685,1686],"td",{},[18,1687,1688],{},"RemovedIn20Warning: The Query.get() method is considered legacy",[1684,1690,1691,1692,1695],{},"Calling ",[18,1693,1694],{},"session.query(Model).get(pk)"," anywhere in the codebase",[1684,1697,1698,1699],{},"Replace with ",[18,1700,1701],{},"session.get(Model, pk)",[1666,1703,1704,1709,1718],{},[1684,1705,1706],{},[18,1707,1708],{},"RemovedIn20Warning: Calling Session.execute() with a string",[1684,1710,1711,1712,1714,1715,1717],{},"Passing a raw string to ",[18,1713,73],{}," instead of a ",[18,1716,522],{}," construct",[1684,1719,1720,1721],{},"Wrap with ",[18,1722,1723],{},"from sqlalchemy import text; session.execute(text(\"SELECT ...\"))",[1666,1725,1726,1731,1737],{},[1684,1727,1728],{},[18,1729,1730],{},"SADeprecationWarning: The declarative_base() function is now considered legacy",[1684,1732,1733,1734,1736],{},"Module-level ",[18,1735,41],{}," call still present",[1684,1738,1739,1740,1742],{},"Subclass ",[18,1741,1192],{}," instead",[1666,1744,1745,1750,1756],{},[1684,1746,1747],{},[18,1748,1749],{},"SADeprecationWarning: The Session.autocommit parameter is deprecated",[1684,1751,1752,1755],{},[18,1753,1754],{},"Session(autocommit=True)"," in session factory",[1684,1757,1758,1759,1761,1762,863,1764,1766],{},"Remove ",[18,1760,858],{},"; use ",[18,1763,862],{},[18,1765,866],{}," explicitly",[1666,1768,1769,1774,1780],{},[1684,1770,1771],{},[18,1772,1773],{},"AttributeError: 'Query' object has no attribute 'get'",[1684,1775,1776,1777],{},"Code running against a 2.0-final engine still calls ",[18,1778,1779],{},"session.query(Model).get()",[1684,1781,1698,1782,1784,1785,1787],{},[18,1783,1701],{}," — ",[18,1786,32],{}," was fully removed",[1666,1789,1790,1795,1802],{},[1684,1791,1792],{},[18,1793,1794],{},"MissingGreenlet: greenlet_spawn has not been called",[1684,1796,1797,1798,1801],{},"Lazy-loading a relationship attribute inside an ",[18,1799,1800],{},"async with AsyncSession"," block",[1684,1803,1804,1805,1808,1809,1812],{},"Add ",[18,1806,1807],{},"lazy=\"selectin\""," to the relationship, or use ",[18,1810,1811],{},"selectinload()"," in your query",[1666,1814,1815,1820,1825],{},[1684,1816,1817],{},[18,1818,1819],{},"InvalidRequestError: A transaction is already begun on this Session",[1684,1821,1691,1822,1824],{},[18,1823,862],{}," twice without committing or rolling back",[1684,1826,1827,1828,1830],{},"Use ",[18,1829,862],{}," only once per unit of work; prefer the context manager form",[1666,1832,1833,1838,1843],{},[1684,1834,1835],{},[18,1836,1837],{},"ArgumentError: relationship 'User.orders' expects a class",[1684,1839,1840,1842],{},[18,1841,1032],{}," string does not match the attribute name on the target class",[1684,1844,1845,1846,1848],{},"Ensure the string passed to ",[18,1847,1032],{}," exactly matches the attribute name defined on the related model",[1666,1850,1851,1856,1865],{},[1684,1852,1853],{},[18,1854,1855],{},"CompileError: Column expression or FROM clause expected, got \u003Cclass 'User'>",[1684,1857,1858,1859,1861,1862,1864],{},"Passing a model class to ",[18,1860,522],{}," or using Core-style ",[18,1863,69],{}," with an ORM entity incorrectly",[1684,1866,1827,1867,1870,1871,1874,1875],{},[18,1868,1869],{},"select(User)"," (ORM select) or ",[18,1872,1873],{},"select(users_table)"," (Core select), not ",[18,1876,1877],{},"text(User)",[57,1879,1881],{"id":1880},"advanced-codemod-optimization","Advanced Codemod Optimization",[14,1883,1884,1887,1888,1891,1892,1894],{},[78,1885,1886],{},"Turn the warning stream itself into your worklist instead of hand-grepping for call sites."," SQLAlchemy does not ship an official AST codemod, and the abandoned ",[18,1889,1890],{},"sqlalchemy-migrate"," package on PyPI is unrelated — it is a 0.x-era schema tool, not a 1.4→2.0 rewriter. The reliable mechanical step is to let the 1.4 ",[18,1893,24],{}," machinery enumerate every offending line for you, because each warning carries the file, line number, and the exact recommended replacement in its message text.",[14,1896,1897],{},"Run your full test suite once with warnings captured to a file, then bucket the results by category so each codemod step becomes a finite, reviewable checklist:",[82,1899,1901],{"className":84,"code":1900,"language":86,"meta":87,"style":87},"# capture_warnings.py — run under SQLALCHEMY_WARN_20=1\nimport warnings\nfrom collections import Counter\n\nrecords: list[warnings.WarningMessage] = []\norig_show = warnings.showwarning\n\n\ndef record(message, category, filename, lineno, file=None, line=None):\n    records.append(\n        warnings.WarningMessage(message, category, filename, lineno, file, line)\n    )\n    orig_show(message, category, filename, lineno, file, line)\n\n\nwarnings.showwarning = record  # install before importing your app\n\n# ... import and exercise your app \u002F run pytest in-process ...\n\nby_site = Counter((r.filename, r.lineno) for r in records)\nfor (path, line), count in by_site.most_common():\n    print(f\"{path}:{line}  x{count}  {records[0].message}\")\n",[18,1902,1903,1908,1915,1927,1931,1941,1951,1955,1959,1982,1987,1998,2002,2011,2015,2019,2032,2036,2041,2045,2068,2081],{"__ignoreMap":87},[91,1904,1905],{"class":93,"line":94},[91,1906,1907],{"class":134},"# capture_warnings.py — run under SQLALCHEMY_WARN_20=1\n",[91,1909,1910,1912],{"class":93,"line":111},[91,1911,105],{"class":97},[91,1913,1914],{"class":101}," warnings\n",[91,1916,1917,1919,1922,1924],{"class":93,"line":124},[91,1918,98],{"class":97},[91,1920,1921],{"class":101}," collections ",[91,1923,105],{"class":97},[91,1925,1926],{"class":101}," Counter\n",[91,1928,1929],{"class":93,"line":131},[91,1930,128],{"emptyLinePlaceholder":127},[91,1932,1933,1936,1938],{"class":93,"line":138},[91,1934,1935],{"class":101},"records: list[warnings.WarningMessage] ",[91,1937,309],{"class":97},[91,1939,1940],{"class":101}," []\n",[91,1942,1943,1946,1948],{"class":93,"line":151},[91,1944,1945],{"class":101},"orig_show ",[91,1947,309],{"class":97},[91,1949,1950],{"class":101}," warnings.showwarning\n",[91,1952,1953],{"class":93,"line":160},[91,1954,128],{"emptyLinePlaceholder":127},[91,1956,1957],{"class":93,"line":166},[91,1958,128],{"emptyLinePlaceholder":127},[91,1960,1961,1963,1966,1969,1971,1973,1976,1978,1980],{"class":93,"line":182},[91,1962,141],{"class":97},[91,1964,1965],{"class":144}," record",[91,1967,1968],{"class":101},"(message, category, filename, lineno, file",[91,1970,309],{"class":97},[91,1972,951],{"class":175},[91,1974,1975],{"class":101},", line",[91,1977,309],{"class":97},[91,1979,951],{"class":175},[91,1981,1074],{"class":101},[91,1983,1984],{"class":93,"line":188},[91,1985,1986],{"class":101},"    records.append(\n",[91,1988,1989,1992,1995],{"class":93,"line":194},[91,1990,1991],{"class":101},"        warnings.WarningMessage(message, category, filename, lineno, ",[91,1993,1994],{"class":461},"file",[91,1996,1997],{"class":101},", line)\n",[91,1999,2000],{"class":93,"line":200},[91,2001,197],{"class":101},[91,2003,2004,2007,2009],{"class":93,"line":205},[91,2005,2006],{"class":101},"    orig_show(message, category, filename, lineno, ",[91,2008,1994],{"class":461},[91,2010,1997],{"class":101},[91,2012,2013],{"class":93,"line":211},[91,2014,128],{"emptyLinePlaceholder":127},[91,2016,2017],{"class":93,"line":237},[91,2018,128],{"emptyLinePlaceholder":127},[91,2020,2021,2024,2026,2029],{"class":93,"line":372},[91,2022,2023],{"class":101},"warnings.showwarning ",[91,2025,309],{"class":97},[91,2027,2028],{"class":101}," record  ",[91,2030,2031],{"class":134},"# install before importing your app\n",[91,2033,2034],{"class":93,"line":1492},[91,2035,128],{"emptyLinePlaceholder":127},[91,2037,2038],{"class":93,"line":1514},[91,2039,2040],{"class":134},"# ... import and exercise your app \u002F run pytest in-process ...\n",[91,2042,2043],{"class":93,"line":1532},[91,2044,128],{"emptyLinePlaceholder":127},[91,2046,2048,2051,2053,2056,2059,2062,2065],{"class":93,"line":2047},20,[91,2049,2050],{"class":101},"by_site ",[91,2052,309],{"class":97},[91,2054,2055],{"class":101}," Counter((r.filename, r.lineno) ",[91,2057,2058],{"class":97},"for",[91,2060,2061],{"class":101}," r ",[91,2063,2064],{"class":97},"in",[91,2066,2067],{"class":101}," records)\n",[91,2069,2071,2073,2076,2078],{"class":93,"line":2070},21,[91,2072,2058],{"class":97},[91,2074,2075],{"class":101}," (path, line), count ",[91,2077,2064],{"class":97},[91,2079,2080],{"class":101}," by_site.most_common():\n",[91,2082,2084,2087,2089,2092,2095,2098,2101,2104,2107,2109,2111,2113,2116,2118,2121,2123,2126,2129,2132,2135,2137,2139],{"class":93,"line":2083},22,[91,2085,2086],{"class":175},"    print",[91,2088,1068],{"class":101},[91,2090,2091],{"class":97},"f",[91,2093,2094],{"class":452},"\"",[91,2096,2097],{"class":175},"{",[91,2099,2100],{"class":101},"path",[91,2102,2103],{"class":175},"}",[91,2105,2106],{"class":452},":",[91,2108,2097],{"class":175},[91,2110,93],{"class":101},[91,2112,2103],{"class":175},[91,2114,2115],{"class":452},"  x",[91,2117,2097],{"class":175},[91,2119,2120],{"class":101},"count",[91,2122,2103],{"class":175},[91,2124,2125],{"class":175},"  {",[91,2127,2128],{"class":101},"records[",[91,2130,2131],{"class":175},"0",[91,2133,2134],{"class":101},"].message",[91,2136,2103],{"class":175},[91,2138,2094],{"class":452},[91,2140,179],{"class":101},[14,2142,2143,2144,2147,2148,2151,2152,2154,2155,2157,2158,2154,2160,2162,2163,2165,2166,2168],{},"This produces a deduplicated, frequency-ranked list of exact ",[18,2145,2146],{},"path:line"," sites — far more precise than ",[18,2149,2150],{},"grep -rn \"session\\.query\"",", which misses dynamically built queries and reports false positives inside strings and comments. Commit each category as its own atomic changeset (one for ",[18,2153,28],{}," → ",[18,2156,69],{},", one for ",[18,2159,32],{},[18,2161,770],{},", and so on) so reviewers can separate mechanical renames from the judgment-intensive steps such as transaction strategy and ",[18,2164,1046],{}," annotations. Pair this with ",[18,2167,2150],{}," only as a final sweep to catch call sites your tests never execute.",[57,2170,2172],{"id":2171},"frequently-asked-questions","Frequently Asked Questions",[14,2174,2175,2178,2179,2181,2182,2184,2185,2188,2189,771,2191,2193],{},[78,2176,2177],{},"Can I migrate one module at a time, or must I update the whole codebase at once?"," You can migrate incrementally. With ",[18,2180,20],{}," on the engine and ",[18,2183,530],{}," set, legacy and 2.0-style code coexist in the same process. Migrate module by module, running your test suite after each batch. The key constraint is that all code sharing a single ",[18,2186,2187],{},"Session"," instance should use consistent patterns — mixing ",[18,2190,28],{},[18,2192,69],{}," inside one unit of work is safe but confusing during review.",[14,2195,2196,2199,2200,2203,2204,2207,2208,2211],{},[78,2197,2198],{},"Do I need to update Alembic as well?"," Yes. Alembic 1.8+ supports SQLAlchemy 2.0. Upgrade Alembic before you upgrade SQLAlchemy: run ",[18,2201,2202],{},"pip install --upgrade alembic sqlalchemy",", then verify your migration scripts still generate correctly with ",[18,2205,2206],{},"alembic revision --autogenerate",". Alembic autogenerate reads ",[18,2209,2210],{},"Base.metadata",", which is unaffected by most codemod steps, so migration history is preserved.",[14,2213,2214,2223,2224,2227,2228,2231,2232,2235],{},[78,2215,2216,2217,2219,2220,2222],{},"What happens to ",[18,2218,919],{}," when I switch to ",[18,2221,862],{},"?"," Nothing changes by default — ",[18,2225,2226],{},"expire_on_commit=True"," is still the default, meaning all attributes are expired after a commit. In async code, accessing an expired attribute after the session closes raises ",[18,2229,2230],{},"MissingGreenlet",". The recommended fix is ",[18,2233,2234],{},"async_sessionmaker(engine, expire_on_commit=False)"," combined with explicit re-fetching when you need fresh data, rather than relying on lazy reload.",[14,2237,2238,2244,2245,2247,2248,2250,2251,2253,2254,2258],{},[78,2239,2240,2241,2243],{},"How do I handle ",[18,2242,38],{}," in third-party models I cannot edit?"," If you inherit from a third-party base that still uses ",[18,2246,38],{},", you cannot remove it without forking the library. Add the relationship on your own subclass using ",[18,2249,1032],{},", and file an upstream issue requesting the migration. SQLAlchemy 2.0 does not break ",[18,2252,38],{}," — it only deprecates it — so you have time to wait for the upstream fix without blocking your own migration. For the full picture of what ",[51,2255,2257],{"href":2256},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fcore-vs-orm-architecture-decisions\u002F","Core vs ORM architecture"," means for third-party integration points, see the architecture decisions page.",[57,2260,2262],{"id":2261},"related","Related",[2264,2265,2266,2277,2284,2291],"ul",{},[2267,2268,2269,2271,2272,771,2274,2276],"li",{},[51,2270,1310],{"href":1309}," — deep dive into ",[18,2273,1046],{},[18,2275,1300],{}," after completing this checklist.",[2267,2278,2279,2283],{},[51,2280,2282],{"href":2281},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Ffixing-removedin20warning-deprecation-warnings\u002F","Fixing RemovedIn20Warning Deprecation Warnings"," — targeted remediation for each warning category the audit step surfaces.",[2267,2285,2286,2290],{},[51,2287,2289],{"href":2288},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fsession-lifecycle-and-scope-management\u002F","Session Lifecycle and Scope Management"," — context for the explicit transaction patterns introduced in Step 5.",[2267,2292,2293,2295,2296,2298],{},[51,2294,1021],{"href":1020}," — production patterns for ",[18,2297,862],{}," and rollback handling across sync and async code.",[2300,2301,2302],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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}",{"title":87,"searchDepth":111,"depth":111,"links":2304},[2305,2306,2326,2327,2328,2329],{"id":59,"depth":111,"text":60},{"id":380,"depth":111,"text":381,"children":2307},[2308,2310,2312,2314,2316,2318,2320,2322,2324],{"id":388,"depth":124,"text":2309},"Step 1 — Enable future=True on create_engine()",{"id":526,"depth":124,"text":2311},"Step 2 — Set SQLALCHEMY_WARN_20=1 and audit all warnings",{"id":582,"depth":124,"text":2313},"Step 3 — Replace session.query() with select() + session.execute()",{"id":758,"depth":124,"text":2315},"Step 4 — Replace Query.get(pk) with Session.get(Model, pk)",{"id":854,"depth":124,"text":2317},"Step 5 — Remove autocommit=True — switch to explicit session.begin() \u002F commit()",{"id":1024,"depth":124,"text":2319},"Step 6 — Update relationship() to use back_populates instead of backref",{"id":1185,"depth":124,"text":2321},"Step 7 — Replace declarative_base() with a DeclarativeBase subclass",{"id":1294,"depth":124,"text":2323},"Step 8 — Replace Column() with mapped_column() + Mapped[T] annotations",{"id":1580,"depth":124,"text":2325},"Step 9 — Remove future=True and unset SQLALCHEMY_WARN_20",{"id":1657,"depth":111,"text":1658},{"id":1880,"depth":111,"text":1881},{"id":2171,"depth":111,"text":2172},{"id":2261,"depth":111,"text":2262},"Migrate from SQLAlchemy 1.4 to 2.0 by enabling future=True, resolving all RemovedIn20Warning emissions, then systematically replacing session.query(), Query.get(), autocommit, backref, declarative_base(), and Column() with their 2.0 equivalents before removing the transitional flag.","md",{"date":2333},"2026-06-18","\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Flegacy-1-4-to-2-0-codemod-checklist",{"title":5,"description":2330},"mastering-sqlalchemy-20-core-and-orm-architecture\u002Fmigrating-legacy-14-code-to-20-syntax\u002Flegacy-1-4-to-2-0-codemod-checklist\u002Findex","9u5T5fmeAMtNMj5yonFIXBlv4tuqK5uj49giMapkkbo",1781810028985]