Repository navigation
Releases
DuckDB v2.0.0
duckdb-python 2.0.0 is the Python package for DuckDB 2.0. It supports Python >= 3.11 and drops Python 3.10. From Python 3.12 on, one wheel per platform serves every Python version. The binding layer was rewritten on nanobind, query results stream instead of being collected up front, the relational API was rebuilt on SQL text, and the way DataFrames, Arrow data and polars frames are read was replaced. Many long-standing bugs are fixed, and moving data in and out of Python is much faster. The engine is DuckDB 2.0.0; see its release notes for what changed in SQL and storage.
Highlights
- Moving data is much faster. Numeric exports to numpy, pandas, Arrow and polars take a quarter to two fifths of the 1.5 time; in-memory Arrow, polars and pandas 3 inputs are scanned on all threads, 8x to 20x faster; Arrow UDFs run about 20x faster; draining an Arrow reader on 16 threads takes an eighth of the time. (#693, #697, #700)
- Results stream.
con.execute()starts the query and you read rows as you fetch them;con.complete()andcon.abort()finish or cancel an open result, and an open result or Arrow reader owns the connection until it is done. - The relational API is rebuilt on SQL text. A relation is a lazy query, bound at every step so its schema is known;
rel.sql_query()shows exactly what will run. Many new methods and expression helpers (col,lit,when, window functions). One semantic change: a plain string in an expression is a value, not a column name. (#697) - A new Appender for fast bulk inserts:
with con.appender("t") as a: a.append_row(...), under a microsecond per row. (#661) - One stable ABI wheel per platform for Python 3.12 and later, so a new Python release works on day one. Python 3.15 is supported. (#672, #673, #684)
- Arrow export on
con.execute()results changed:fetch_arrow_table,fetch_record_batchandarrow(n)are gone; usecon.sql(query).to_arrow_table()or.to_arrow_reader(). (#702) - The experimental Spark API is brought up to date: it is now compatible with PySpark 4.2.0 and 3.5.9 (#686). We would love your input on whether to continue support or to drop it in future releases.
Upgrading from 1.x: what to check
- Make sure you run Python 3.11 or later.
- If you call
fetch_arrow_table,fetch_record_batch,arrow(n)orpl()on the result ofcon.execute(), switch tocon.sql(query).to_arrow_table(),to_arrow_reader()orpl(). Libraries that call the removed names on an execute result need an update. (#702) - If you call
fetchone(),fetchmany(),fetch_df_chunk()orexecute()on a relation, userel.limit(n).fetchall(),rel.to_arrow_reader(),rel.materialize(), or run the query throughcon.execute(). (#697) - If you compare a relation column with a plain string,
col("s") == "x"now compares with the textx. Wrap column names incol(). (#697) - If you rely on
import duckdbhaving opened the default database, note that it happens on the firstduckdb.sql()orduckdb.connect()call instead. (#680) - If you pass
Decimalvalues with a positive exponent, such asDecimal("1E+2"), they now arrive as the number they denote, 100, where 1.5 produced 1.00. (#654) - If you write files with
overwrite=False, an existing file is now refused instead of replaced. (#668) - If a test or script depends on
str(rel)being free, note that it runs the query each time now.
Breaking changes
Arrow export on DB-API results (#702)
Results of con.execute() are now read row by row and are not kept in memory, so they cannot hand their data to Arrow for free. fetch_arrow_table, fetch_record_batch, arrow(rows_per_batch) and pl() on a DB-API result are removed; they had been deprecated. to_arrow_table() and to_arrow_reader() remain, also as duckdb.to_arrow_table() and duckdb.to_arrow_reader(), but they materialize the rows twice, once as the result and once as Arrow, and emit a UserWarning pointing at the relational path, con.sql(query).to_arrow_table(), which we recommend instead. The reader no longer streams: its first batch arrives after the whole query ran. After a partial fetchone or fetchmany the Arrow call returns the remaining rows instead of raising. With preserve_insertion_order off, the row order can differ between calls. duckdb.arrow(n) with an integer is gone; duckdb.arrow(arrow_object) still builds a relation.
# 1.5
table = con.execute("SELECT * FROM t").fetch_arrow_table()
reader = con.execute("SELECT * FROM t").fetch_record_batch(10_000)
# 2.0
table = con.sql("SELECT * FROM t").to_arrow_table()
reader = con.sql("SELECT * FROM t").to_arrow_reader(10_000)Relations are SQL text (#697)
The relational API is a query building API, not an execution API. A relation is a piece of SQL text with a schema bound once when it is built, so every step knows its columns and types. It runs nothing until you ask for data, and it runs again on every read; rel.sql_query() returns exactly the text that runs, as one SELECT per group of steps and a common table expression where the order of clauses breaks. The engine's binder decides every column name and type; nothing is renamed by the client.
What you are most likely to notice:
-
A bare string operand of an expression is a value, not a column. For one release a warning is raised when the string matches a column name; the warning goes in 2.1. A dotted string is not split into parts; pass the parts separately.
from duckdb import ColumnExpression as c rel.filter(c("s") == "x") # 1.5: s = x when a column x exists; 2.0: s = 'x' rel.filter(c("s") == c("x")) # both: s = x
-
fetchone(),fetchmany(),fetch_df_chunk()andexecute()are gone from relations. The error names the replacement:limit(1).fetchall(),to_arrow_reader()orlimit(n).df()for batches, andmaterialize()to run a relation once and keep its rows. -
con.sql(text, params=...)is lazy and binds its parameter values on every read; it used to run once. -
con.sql()of a statement that returns no rows, such asCREATE TABLEor anINSERTwithoutRETURNING, returns a relation over what the statement returned instead ofNone: no rows for DDL, the changed row count for DML. The statement still runs at the call. Only SELECT-shaped statements are lazy;CALL,PRAGMA,CHECKPOINTandEXPLAINrun at the call and the relation holds their output. -
Over a join,
l.xandr.yresolve inside the join's own SELECT. Once a later step starts a new SELECT the input alias is no longer known and the engine's error is raised. Two columns of the same name arenameandname_1past that point. -
A persistent view over held rows (
materialize(),values(), a statement that ran at the call) is refused; userel.create("name")orcon.register("name", rel). -
A relation over a file pins its schema when it is built; a file that changes between building and reading no longer yields rows that disagree with the declared columns. A reader with
filename=Trueor a Hive partition declares that column among its columns. -
The new verb names are methods, so a column called
offset,sample,head,rename,pivot,drop,unnest,unpivot,previeworgroup_byis reached asrel["offset"].
Further differences against 1.5
rel.query(name, sql)leaves no temporary view behind.- A SELECT the engine classifies as writing, such as
SELECT nextval('s'), runs at the call and the relation holds its rows. values()converts its rows once at the call;sql_query()shows a reference to them, not a VALUES list.- A mistake in a lazy
con.sql()SELECT, such as a misspelled column, no longer aborts an open transaction, because building a relation runs nothing. - A relation re-reads its columns after the connection ran a statement that could have changed the catalog, so a filter on a column added by
ALTER TABLEworks. - A relation can be read by name from SQL on any connection of the same database, including cursors; 1.5 refused a relation made on another connection. A connection to a different database still refuses.
bool(expression)raisesTypeError; combine conditions with&,|and~. An operand no expression can hold raisesTypeErrorwhere==used to returnFalse.ConstantExpression({1: "a"})is a MAP; 1.5 made a STRUCT with the key"1".update(set={...})refuses a bare string value; writecol("name")to copy a column orlit("x")for text.str(expression)quotes identifiers, parenthesizes operations and writes literals in; the text differs from 1.5 but means the same.str(rel)runs the query on every call; 1.5 cached the first rendering.- A non-contiguous
memoryviewgiven tovalues()or an expression is read as a BLOB in logical byte order, where 1.5 raisedBufferError. As a query parameter it is still refused. unregister(name)raises when it cannot remove its view instead of reporting success and leaving it behind.- A connection object that becomes garbage while the connection is still used elsewhere, for example the default connection reachable only through a reference cycle, no longer closes that connection when the cyclic collector frees the object.
Executing statements through the DB-API (con.execute)
A query is now started at execute() and read as you fetch, instead of being run to completion first. An error in the first rows still raises from execute(); an error later in the query raises from the fetch call that reaches it. Statements that write, including RETURNING, complete inside execute(). Calling execute() again while a result is open replac...
v1.5.6 Bugfix Release
See DuckDB's changelog for all changes in DuckDB.
v1.5.5 Bugfix Release
See DuckDB's changelog for all changes in DuckDB.
What's Changed in DuckDB-Python
- Fix numpy deprecations by @evertlammerts in #505
- Fix alias of
DuckDBPyRelation.query(closes #468) by @evertlammerts in #524
v1.5.4 Bugfix Release
See DuckDB's changelog for all changes in DuckDB.
What's Changed in DuckDB-Python
- Make rel->query work with a read only connection by @evertlammerts in #471
- Ignore Polars dynamic predicates by @evertlammerts in #482
- Accept pathlib.Path, os.PathLike, bytes, and file-like objects in read_parquet/from_parquet by @evertlammerts in #489
- Unify arrow exports across all query result types by @evertlammerts in #495
v1.4.5 Bugfix Release
This is a patch release for the v1.4 LTS line. See DuckDB's changelog for all changes in DuckDB.
What's changed in DuckDB-Python
- Fix a memory leak on every Python scalar UDF call.
- Fix data corruption when reading from fsspec filesystems concurrently (seek+read/write are now atomic).
- Fix incorrect results from Arrow/PyArrow filter pushdown when an OR predicate contains a dynamic filter (e.g.
ORDER BY ... NULLS FIRST LIMIT n), reported in issue #460. - Raise a clear error instead of segfaulting when None is passed to
Relation.join.
v1.5.3 Bugfix Release
See the DuckDB core release notes here: https://github.com/duckdb/duckdb/releases/tag/v1.5.3
What's Changed in DuckDB Python
- Export all symbols by @evertlammerts in #445
- Fix concjunction OR by @evertlammerts in #465
- Use recursive mutex to deal with GIL <-> internal lock deadlocks by @evertlammerts in #462
- Allow self-joining of Polars lazyframes by @evertlammerts in #466
Full Changelog: v1.5.2...v1.5.3
v1.5.2 Bugfix Release
See the DuckDB core release notes here: https://github.com/duckdb/duckdb/releases/tag/v1.5.2
What's Changed in duckdb-python
- Type hints overhaul by @OutSquareCapital in #352
- Cache arrow schema for streaming types by @evertlammerts in #423
- Fix segfault on join with None by @evertlammerts in #422
- Fix stubs for path_or_buffer param by @evertlammerts in #425
v1.5.1 Bugfix Release
DuckDB core v1.5.1 Changelog: duckdb/duckdb@v1.5.0...v1.5.1
What's Changed
- Re-added basic support for the GEOMETRY type
DuckDB Python 1.5.0 "Variegata"
This is the 1.5.0 release of DuckDB's Python bindings. For a list of changes in DuckDB core, have a look at the DuckDB release notes and the blogpost.
Breaking Changes
- Dropped Python 3.9 support. The minimum supported version is now Python 3.10.
- Removed deprecated
duckdb.typingandduckdb.functionalmodules. These were deprecated in 1.4.0. Useduckdb.sqltypesandduckdb.funcinstead. - Renamed
columnparameter toexpressionin relational API functions (e.g.,min,max,sum,mean, etc.) to better reflect that these accept expressions, not just column names. - Deprecated
fetch_arrow_table()andfetch_record_batch()on connections and relations. Use the newto_arrow_table()andto_arrow_reader()methods instead.
New Features
- Polars LazyFrame projection and filter pushdown. DuckDB can now push down projections and filters when scanning Polars LazyFrames, including support for cast nodes and unstrict casts.
- Polars Int128 / UInt128 support.
- VARIANT type support — Python conversion, NumPy array wrapping, and type stubs.
- TIME_NS type support — nanosecond-precision time values across Python, NumPy, and Spark type systems.
- Profiling API — new
get_profiling_info()andget_profiling_json()methods on connections, plus a refactoredquery_graphmodule with improved HTML visualization (dark mode, expandable phases, depth). to_arrow_table()andto_arrow_reader()— new methods on connections and relations as the preferred Arrow export API.
Performance
__arrow_c_stream__on relations — relations now export via the Arrow PyCapsule interface usingPhysicalArrowCollectorfor zero-copy streaming.- Unified Arrow stream scanning via
__arrow_c_stream__, with filter pushdown only when pyarrow is present. - Arrow schema caching to avoid repeated lookups during scanning.
- Arrow object type caching to avoid repeated detection.
- Empty params treated as None for
.sql()— avoids unnecessary parameter binding overhead. - Simplified GIL management for
FetchRow.
Bug Fixes
- Fixed Python object leak in scalar UDFs —
PyObject_CallObjectreturn values are now properly stolen to avoid reference count leaks. - Fixed reference cycle between connections and relations that could prevent garbage collection.
- Relations now hold a reference to their connection, preventing premature connection closure.
- Fixed fsspec race condition in the Python filesystem implementation.
- Fixed numeric conversion logic — improved handling of large integers (fallback to VARCHAR) and UNION types.
pyarrow.datasetimport is now optional — no longer fails if pyarrow is installed without the dataset module.- Thrown a reasonable error when an Arrow array stream has already been consumed.
Build & Packaging
- jemalloc enabled on Linux x86_64 only (aligned with DuckDB core), removed as a separately bundled extension.
- MSVC runtime linked statically on Windows — eliminates the VS2019 workaround from duckdb/duckdb#17991.
v1.4.4 Bugfix Release
DuckDB core v1.4.4 Changelog: duckdb/duckdb@v1.4.3...v1.4.4
What's Changed in the Python Extension
- fix polars tests by @evertlammerts in #218
- tests for string and binary views by @evertlammerts in #221
- Quote view names in unregister by @evertlammerts in #222
- Limit string nodes in Polars expressions to constant expressions by @evertlammerts in #225
- Escape identifiers in relation aggregations by @evertlammerts in #272
- Fix DECREF bug during interpreter shutdown by @evertlammerts in #275
- Support for Pandas 3.0.0 by @evertlammerts in #277
- Prepare for v1.4.4 by @evertlammerts in #280
Full Changelog: v1.4.3...v1.4.4