2.1.0rc1
Released: August 31, 2026
platform
-
[platform] [change] Python 3.11 or above is now required; support for Python 3.10 is dropped, in addition to the drop of versions Python 3.9, 3.8 and 3.7 introduced in 2.1.0b1. Python 3.10 reaches EOL in October of 2026, so dropping support now gives the SQLAlchemy 2.1 series an extra year of space to remain on current Python versions.
-
[platform] [bug] Python 3.15 support has been added and tested, including minimal changes for full compatibility.
This change is also backported to: 2.0.52
References: #13477
orm
-
[orm] [usecase] Improved the error message raised when a
Sessionis used inside a context manager after the transaction has been rolled back due to an exception. TheInvalidRequestErrornow includes the original exception that triggered the rollback, making it clearer why the transaction is no longer active. Pull request courtesy Ilan Keshet.References: #11297
-
[orm] [usecase] Improved error messages raised when ORM loader strategy options cannot be applied to a query. Messages now render the offending option in a user-friendly form such as
joinedload(User.orders)rather than exposing internal class and path representations, and the "does not apply to root entities" message now includes the option that triggered the error. The same user-friendly rendering is also applied to the "conflicting loader strategy" message and to theof_type()representation in "does not link" messages. Originating pull request courtesy Jan Vollmer.References: #12398
-
[orm] [usecase] Python source generated at runtime is now compiled against a descriptive filename which is registered with the
linecachemodule, so that generated functions appearing on a stack trace render with their source rather than as an opaqueFile "<string>"frame. This allows tools likepdbandinspect.getsource()to work with these generated source blocks as well. The new feature is applied to the instrumentation applied to an ORM object's__init__method, as well as throughout SQLAlchemy functions that are internally instrumented.References: #13505
-
[orm] [usecase] When a subclass overrides a
_orm.validates()method using the same method name as the parent class, only the subclass validator is now invoked for instances of the subclass. The subclass validator may callsuper()to also invoke the parent class validator. Previously, the parent validator was always used regardless of whether the subclass provided an override. Pull request courtesy Indivar Mishra.References: #2943
-
[orm] [bug] Fixed a result-column misalignment bug in ORM-enabled UPDATE statements where
synchronize_session="fetch"is in use, either explicitly or because the statement uses constructs such as CTEs that implicitly select for it. Columns in rows returned by.returning()could be returned under incorrect keys (e.g.row[SomeClass.a]returning the value of a different column), a problem most likely to manifest under concurrent workloads. ORM DELETE statements were not affected.This change is also backported to: 2.0.52
References: #13439
-
[orm] [bug] Fixed bug where a failed
_orm.Session.bulk_insert_mappings(),_orm.Session.bulk_update_mappings()or_orm.Session.bulk_save_objects()call could leave the_orm.Sessionpermanently in a "flushing" state, such as when the transaction could not be begun because a previous flush had left it needing a rollback. Unlike_orm.Session.flush(), the bulk methods set the internal flushing flag and began the transaction outside of thetry/finallyblock that resets it, so that neither_orm.Session.rollback()nor_orm.Session.close()would clear it, and every subsequent flush would raiseInvalidRequestError: Session is already flushing. Pull request courtesy Hamody We.This change is also backported to: 2.0.52
References: #13485
-
[orm] [bug] Fixed issue where unpickling an ORM object that were loaded using loader options making use of wildcard tokens, such as
_orm.load_only()or_orm.raiseload()with"*", would fail withKeyErrororIndexErrorif the process doing the unpickling had not yet constructed a loader path making use of that same token. This would typically be observed when the object were unpickled in a separate process, such as with thespawnorforkservermultiprocessing start methods, the latter of which became the default on POSIX platforms as of Python 3.14. The internal collection of these tokens is now established up front, so that it is identical in every process.This change is also backported to: 2.0.52
References: #13493
-
[orm] [bug] Fixed issue where a string ending in
"*"passed to a_orm.Loadstrategy method, such asLoad(A).joinedload("bs.*"), would bypass the check which rejects string attribute names in loader options, silently producing a loader path that matched nothing. Such a string now raisesArgumentErrorwith the same message given for any other string attribute name. The bare wildcard"*", as inLoad(A).lazyload("*"), continues to be accepted.This change is also backported to: 2.0.52
References: #13493
-
[orm] [bug] Calling
_orm.aliased()against a_sql.select()or_sql.union()/_sql.CompoundSelectconstruct, which previously failed with an obscureAttributeErrorregarding a missing.mapperattribute, now raises when using SQLAlchemy 2.1, and emits a deprecation warning under SQLAlchemy 2.0 as it coerces the construct into a subquery instead. This matches the behavior of other similar implicit SELECT-to-FROM coercions. Pull request courtesy Rens Groothuijsen.This change is also backported to: 2.0.52
References: #6274
-
[orm] [bug] [regression] Fixed regression caused by the dataclasses change in #12168 where passing
_orm.relationship.default_factoryaslistto a relationship that used the_orm.WriteOnlyMappedor_orm.DynamicMappedannotation would raise an error at mapper configuration time, as these relationships have nocollection_class.listis now accepted for these relationships, which behave the same as ordinary collections in this regard; the factory itself is never invoked, and a newly constructed object begins with an empty collection. Documentation is added atwrite_only_dataclassesillustrating the use of write only and dynamic relationships with ORM mapped dataclasses.References: #13227
-
[orm] [bug] Fixed long-standing issue where an object that was loaded at more than one path within a single query, such as when a chain of
_orm.joinedload()options leads back to an entity that was also loaded at the top level of the query, would retain the loader options of whichever path the query happened to see last, which varied with the loader strategy in use. The options an object retains are applied to all forms of :term:lazy loadingfor that object, so an otherwise identical set of options could behave differently depending on the loader strategy. The shallowest path is now favored, which is deterministic.Unknown interpreted text role "term".
References: #13507
engine
-
[engine] [bug] [asyncio] [pool] Fixed issue where a DBAPI connection would be left open and unreachable if an exception were raised within the
_events.PoolEvents.connect()or_events.PoolEvents.first_connect()event handlers, which is whereDialect.initialize()runs. The connection had been created but not yet associated with anything that could close it, so it was neither returned to the pool nor closed. For an asyncio driver in particular this could leak a server-side session for the life of the process, as the garbage collector is not able to close a connection that requires the event loop.This change is also backported to: 2.0.53
References: #13548
sql
-
[sql] [usecase] Added new methods
_sql.Exists.with_hint()and_sql.Exists.with_statement_hint(), which apply a table hint or a statement hint to the SELECT statement that's enclosed by the EXISTS expression, in the same way as_sql.Select.with_hint()and_sql.Select.with_statement_hint(). As ORM constructs such as_orm.PropComparator.any()and_orm.PropComparator.has()produce an_sql.Existsobject, hints may now be applied to the subqueries which these constructs generate. Pull request courtesy Abhinav Gorrepati.References: #8311
-
[sql] [performance] Improved the performance of SQL cache key generation by moving the traversal into the Cython extension modules. The set of attributes that participate in the cache key for a particular construct, along with the handler that applies to each one, is now resolved once at class setup time into a structure that the compiled traversal consumes directly, so that the chain of identity comparisons that formerly rediscovered this per attribute, per cache key is no longer run at all. Benchmarks against a range of Core and ORM statements show cache key generation running approximately 1.5 to 2.2 times faster in a build with the Cython extensions compiled, and approximately 1.05 to 1.2 times faster in a pure Python build, with the generated cache keys themselves unchanged.
References: #13506
-
[sql] [bug] Fixed an issue in
Numericwhere theNumeric.decimal_return_scaleparameter was ignored when the DBAPI does not support native decimal objects (i.e.dialect.supports_native_decimalisFalse). In this path the result processor was computing the conversion scale fromNumeric.scaledirectly, bypassingNumeric.decimal_return_scaleentirely. The behavior now matchesFloat, which already used the correct_effective_decimal_return_scaleproperty. Pull request courtesy Kadir Can Ozden.This change is also backported to: 2.0.52
References: #13424
-
[sql] [bug] Added auditing to the test suite which exercises the literal execute processors across all datatypes and dialects to ensure that string input is either appropriately rejected or correctly escaped. Literal execute processors are invoked when the
bindparam.literal_executeparameter is used with an explicitbindparam()object, which overrides DBAPI-native bind handling to render the value inline with the statement instead. Datatypes that were updated include the originally reported SQL ServerUuid/UNIQUEIDENTIFIERrendering which now escapes properly, theJSONPATHtype that's currently PostgreSQL-only, and a full family of numeric types stemming from the_types.Floatand_types.Numericbases which now coerce the value to a number, rejecting non-numeric input. Thanks to Javid Khan for helping to identify the issue.This change is also backported to: 2.0.52
References: #13448
-
[sql] [bug] Fixed issue where two bound parameters whose names differ only in the characters listed in
SQLCompiler.bindname_escape_characters, such as those generated for columns named"a.b"and"a_b", would be rendered using the same name in the compiled statement, as those characters are escaped only as the parameter is rendered. The value for one of the two parameters was then silently used for both, affecting SELECT criteria as well as the VALUES and SET clauses of INSERT and UPDATE statements, where a value could be written to the wrong column. Escaped parameter names are now disambiguated against the names already in use. The.keyof eachBindParameteris unaffected, so parameter dictionaries passed by the caller continue to be keyed as before.References: #13534
-
[sql] [bug] Fixed issue where an empty string passed to
IdentifierPreparer.quote(), such as the name of aTableconstructed with a blank name, would raiseIndexErrorrather than being rendered. An empty identifier is now always quoted. While a blank name is not a legal identifier on most backends, SQLite accepts one, so such a table may be delivered by reflection; a table with a blank name can now be used in SELECT, INSERT, UPDATE, DELETE and DDL statements.References: #13535
schema
-
[schema] [usecase]
ForeignKeyConstraintnow accepts a constraint which names the same local column more than once, such asFOREIGN KEY (a, a) REFERENCES r (b, c). This form is valid SQL and constrains the referenced row so that two of its columns are equal; it previously raisedArgumentError. Such a constraint now emits and reflects like any other composite foreign key; the workaround added in 2.0 for #13525, which skipped such a constraint during reflection, is removed as it is no longer needed. As part of this change, the check that the number of constrained columns matches the number of referenced columns no longer counts distinct column names, so that a genuine mismatch such asForeignKeyConstraint(["x", "x"], ["r.b"]), which was formerly accepted and silently dropped a column, is now rejected.References: #13526
-
[schema] [usecase] Added new
_schema.ForeignKeyaccessors_schema.ForeignKey.target_tokens,_schema.ForeignKey.target_columnand_schema.ForeignKey.target_table_key, as well as new ForeignKey-related datastructure_schema.ForeignKeyTarget._schema.ForeignKeyTargetis now accepted as a constructor argument as well. See_schema.ForeignKeyfor new datamembers and usage patterns.References: #13538
-
[schema] [usecase] Added
Dialect.dbapi_version, a standardized accessor for the version of the DBAPI module in use by a dialect, in contrast toDialect.server_version_infowhich refers to the database server. The implementation onDefaultDialectmakes use of a new per-dialect methodDialect.retrieve_dbapi_version()in order to retrieve the version from the DBAPI module and return it as aVersionInfoobject, which is a tuple subclass with additional properties; third-party dialects should also implement theDialect.retrieve_dbapi_version()method. -
[schema] [performance] Created new reflection method
_reflection.Inspector.has_multi_table()to check the existence of multiple tables at once, allowing for better performance when checking many tables. Like the other "multi" reflection methods, the default dialect offers a default implementation that just call the single method in a loop. Backends that wish to take advantage of this new method can implement it in their dialects. The PostgreSQL, Oracle and SQL Server dialects have been updated to use this new method. The implementation of_schema.MetaData.create_all()has been updated to make use of this new method to check the existence of the tables, reducing the number of round trips to the database when creating many tables.References: #13311
-
[schema] [bug] Fixed an issue where
_schema.Table.to_metadata()reused column default and on-update objects, causing the defaults on the original columns to refer to the copied columns. Default generators, including sequences, and server-side defaults are now copied and remain associated with their respective columns and metadata collections. Applications that inspected these objects will now see distinct defaults on the copied table instead of the objects owned by the original table. Pull request courtesy Goutam Adwant.This change is also backported to: 2.0.52
References: #13481
-
[schema] [bug] Fixed issue where a
_schema.ForeignKeywhich refers to a table or column whose name contains a dot would be interpreted incorrectly, as the dotted string form of the target could not be told apart from the separator between a schema, table and column name. Foreign key targets are now tracked as their individual schema, table and column names throughout, and are no longer derived by splitting a dotted string.References: #13538
-
[schema] [deprecated]
_schema.ForeignKey.target_fullnameis now a legacy accessor, and raisesInvalidRequestErrorwhen the target has no unambiguous dotted string form, which is the case when the target table or column name contains a dot, or when a schema name is present with no column name. No SQLAlchemy internals make use of the attribute any longer; new code should use_schema.ForeignKey.target_tokens.References: #13538
postgresql
-
[postgresql] [usecase] Added
postgresql_withsupport toCreateViewfor specifying PostgreSQL view options such assecurity_invoker,security_barrier, andcheck_option, rendered as aWITH (...)clause between the view name and theASkeyword. Additionally, thepostgresql_withparameter accepted by_schema.Tableand_schema.Indexnow correctly renders Python boolean values astrue/false(lowercase), andNonevalues as the parameter name alone without an= valueportion. Pull request courtesy alphavector. -
[postgresql] [usecase] The PostgreSQL dialect now reflects the schema of a schema-qualified column or
_postgresql.DOMAINcollation, populating the newString.collation_schema/_postgresql.DOMAIN.collation_schemaparameters so that reflected DDL round-trips exactly. The schema is omitted from the reflected value when the collation is visible on the currentsearch_pathwithout qualification.References: #6511
-
[postgresql] [usecase] Added a new parameter
String.collation_schema, as well as_postgresql.DOMAIN.collation_schemaandColumnOperators.collate.collation_schema, allowing a PostgreSQL schema-qualified collation name to be specified explicitly, rather than embedding the schema name within thecollationstring itself, which previously rendered incorrectly. As part of this change, collation name rendering across DDL and the_sql.collate()construct now consistently uses the dialect's identifier preparer for quoting, rather than several separate, inconsistent hand-quoting code paths; as a side effect, simple lowercase collation names such as"utf8"are no longer unconditionally quoted in generated DDL.References: #9693
-
[postgresql] [bug] Fixed bug in
_reflection.Inspector.get_schema_names()for PostgreSQL where the query used to exclude system schemas relied onNOT LIKE 'pg_%', which treats the underscore as a SQLLIKEwildcard rather than a literal character. This caused user-created schemas that happen to start with "pg" followed by any other character, such aspgsqlorpgstats, to be silently excluded along with actual system schemas likepg_catalog. Pull request courtesy Evan Rusackas.This change is also backported to: 2.0.53
References: #13472
-
[postgresql] [bug] [reflection] Fixed reflection of PostgreSQL CHECK constraints where an expression made up of multiple parenthesized sub-expressions, such as
(x IS NULL OR y IS NULL) AND (x IS NULL OR y IS NULL), would have its leading and trailing parentheses incorrectly stripped, producing an unbalanced and syntactically invalid reflected expression. Pull request courtesy Shaurya Singh.This change is also backported to: 2.0.52
References: #13157
-
[postgresql] [bug] Fixed bug in the PostgreSQL dialect where a single quote in a sequence, table, or schema name, such as one supplied via a
schema_translate_mapor an explicitSequence, could result in a malformednextval()statement. The quote is now properly escaped. Pull request courtesy dxbjavid.This change is also backported to: 2.0.52
References: #13429
-
[postgresql] [bug] Fixed issue in the asyncpg dialect where the version of the
asyncpgDBAPI would always be reported as(99, 99, 99), as the version was looked up on the dialect's DBAPI wrapper module rather than on theasyncpgmodule itself.
mysql
-
[mysql] [bug] Ensure that CREATE TABLE DDL statements for MySQL and MariaDB dialects render the table options in a deterministic order. Previously the order could change depending on the Python seed.
This change is also backported to: 2.0.53
References: #13523
-
[mysql] [bug] Fixed issue where the version of the DBAPI reported by the mysqldb and pymysql dialects was incorrect. Current mysqlclient releases publish
MySQLdb.version_infoand no version string at all, so no version was reported; pymysql publishes__version__andversion_infoas mysqlclient compatibility values, so the version reported for pymysql was that of the mysqlclient release it emulates, e.g.(2, 2, 8)rather than(1, 2, 0).
sqlite
-
[sqlite] [usecase] Added support for multiple
ON CONFLICTclauses within a single statement for the SQLite_sqlite.insert()construct; the_sqlite.Insert.on_conflict_do_update()and_sqlite.Insert.on_conflict_do_nothing()methods may now each be invoked more than once against the same construct, where the clauses render in the order in which they were established and are evaluated by SQLite in that order. As SQLite allows only the lastON CONFLICTclause to omit its conflict target, a_sqlite.Insert.on_conflict_do_nothing()call that omits_sqlite.Insert.on_conflict_do_nothing.index_elementsmust be the last clause established. Documentation is added atsqlite_on_conflict_multiple. Pull request courtesy Diemid Berozkin.References: #13113
-
[sqlite] [bug] [reflection] Fixed issue in SQLite reflection where the name of a
PRIMARY KEY,UNIQUEorFOREIGN KEYconstraint would be reflected asNoneif theCONSTRAINT <name>clause were separated from the keyword that follows it by a newline rather than by spaces. As SQLite stores theCREATE TABLEstatement as it was originally typed, this affected tables created from hand-written DDL that spans multiple lines. The regular expressions used to recover constraint names, as well as theON UPDATE/ON DELETE,DEFERRABLEandINITIALLYoptions of a foreign key constraint, now accept any whitespace between tokens.This change is also backported to: 2.0.53
References: #13528
-
[sqlite] [bug] Reworked the regular expression that detects inline
UNIQUEcolumn constraints during SQLiteCREATE TABLEreflection so that the whitespace separating a column's type from a following clause is matched unambiguously. The previous pattern had three overlapping quantifiers that could each consume a space character, so a column definition carrying a long run of whitespace in the stored schema made_reflection.Inspector.get_unique_constraints()spend cubic time backtracking before returning. Fix courtesy of Javid Khan.This change is also backported to: 2.0.52
References: #13419
-
[sqlite] [bug] Added a warning for query string arguments that are passed to a SQLite URL without the
uri=trueargument also being present, and which are not accepted by thesqlite3driver itself. SQLite URI arguments such asmodeorcachetake effect only when URI mode is in use; without it they were previously discarded silently, so that a URL such assqlite:///file:mydb?mode=memorywould connect to a file on disk namedfile:mydb. Arguments intended for the driver itself may be passed using the_sa.create_engine.connect_argsparameter.References: #13433
-
[sqlite] [bug] [documentation] Corrected the SQLite documentation regarding shared cache memory databases, which incorrectly indicated that the named form
sqlite:///file:mydb?mode=memory&cache=shared&uri=truemakes use ofQueuePool; a single-connection pool is used for this form. Documentation has also been added noting that a shared cache database exists only for as long as at least one connection to it remains open, so that ordinary pool operations such as_engine.Engine.dispose()or use of_sa.create_engine.pool_recyclewill discard its contents.References: #13433
-
[sqlite] [deprecated] Deprecated the selection of a single-connection pool class, i.e.
SingletonThreadPoolfor pysqlite orStaticPoolfor aiosqlite, based on the presence of themode=memoryquery string argument in a SQLite URL. Pool selection for SQLite is intended to be based on the database name alone, where only:memory:or an empty database name indicate a memory database; interpreting the query string additionally requires that assumptions be made regarding whether or not the resulting database can be shared among multiple connections. In a future release, such URLs will make use ofQueuePoolorAsyncAdaptedQueuePoolas would any other URL. This notably includes the shared cache formsqlite:///file:mydb?mode=memory&cache=shared&uri=true, for which a queue pool is in fact the appropriate class, as a shared cache database supports multiple concurrent connections, whereas a single-connection pool causes such connections to share one transaction state. Applications that rely upon the present behavior should indicate the intended pool using the_sa.create_engine.poolclassparameter. Pull request courtesy Itachi-0xAI.References: #13433
mssql
-
[mssql] [bug] [reflection] Fixed issue in SQL Server reflection where
TEXTandNTEXTcolumns would be reflected with a spurious length of 16 and 8, respectively. These are unlengthed LOB datatypes; the value originates from thesys.columns.max_lengthcolumn, which reports the size of the in-row LOB pointer rather than a character length for these types. The reflected_mssql.TEXTand_mssql.NTEXTtypes now have alengthofNone, so that a reflected table emits valid DDL when re-created, which previously failed with "Cannot specify a column width on data type text". Pull request courtesy Sam Debruyn.This change is also backported to: 2.0.53
References: #13451
-
[mssql] [bug] Improved disconnect detection for the
mssql+mssqlpythondialect. Connection-level failures such as a dropped or reset network connection are now recognized by consulting thedriver_errorattribute of the exception, in addition to the message-based checks that were already in place, so that the affected connection is invalidated and the pool "pre ping" feature is able to recycle it. Pull request courtesy Sam Debruyn.References: #13441
oracle
-
[oracle] [bug] Updated the oracledb async dialect where the async cursor adapter invoked
__enter__()rather than__aenter__()on the underlying cursor. While these are equivalent in oracledb itself, the correct async form is now used for correctness. AsAsyncCursor.__aenter__()was added in oracledb 2.0.1, the minimum supported oracledb version is now 2.0.1, declared via theoracle-oracledbextra. Pull request courtesy AVRC26.References: #13420
-
[oracle] [bug] Fixed issue in the Oracle dialects where a
_types.JSONvalue would be returned as an undecoded string for any JSON expression that is not a JSON column, such as a bound parameter, as well as for textual constructs with positional columns, such as_expression.text()combined with_expression.TextClause.columns().References: #13479
tests
-
[tests] [usecase] The version specifications used by testing exclusions such as
testing.fails_if("+asyncmy<0.2.13")now support a driver name, in which case the comparison is against the version of the DBAPI rather than that of the database server. Previously this form raisedAssertionError: DBAPI version specs not supported yet. -
[tests] [bug] Altered the dialect reflection test
test_check_constraint_parenthesized_expressions()so that it does not convert the reflected constraint to lowercase, which interferes with some third party dialect's representation of reflected check constraints.This change is also backported to: 2.0.53
References: #13521
misc
-
[bug] [installation] Added the
AUTHORSfile to the set of license files included in the built wheel, where previously only theLICENSEfile was present. As the text ofLICENSErefers toAUTHORSfor the list of copyright holders, the reference would not resolve for tools that inspect an installed distribution.This change is also backported to: 2.0.53
References: #13518
-
[misc] [bug] Allowed the inspection registry to replace an existing registration with a reloaded callable from the same module and name. This avoids an assertion failure for tooling that unloads and reloads SQLAlchemy modules while still rejecting conflicting registrations. Pull request courtersy w-Jessamine.
References: #10748