Skip to content

Releases

DuckDB v2.0.0

Choose a tag to compare

@evertlammerts evertlammerts released this 21 Oct 12:51
83c7199

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() and con.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_batch and arrow(n) are gone; use con.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

  1. Make sure you run Python 3.11 or later.
  2. If you call fetch_arrow_table, fetch_record_batch, arrow(n) or pl() on the result of con.execute(), switch to con.sql(query).to_arrow_table(), to_arrow_reader() or pl(). Libraries that call the removed names on an execute result need an update. (#702)
  3. If you call fetchone(), fetchmany(), fetch_df_chunk() or execute() on a relation, use rel.limit(n).fetchall(), rel.to_arrow_reader(), rel.materialize(), or run the query through con.execute(). (#697)
  4. If you compare a relation column with a plain string, col("s") == "x" now compares with the text x. Wrap column names in col(). (#697)
  5. If you rely on import duckdb having opened the default database, note that it happens on the first duckdb.sql() or duckdb.connect() call instead. (#680)
  6. If you pass Decimal values with a positive exponent, such as Decimal("1E+2"), they now arrive as the number they denote, 100, where 1.5 produced 1.00. (#654)
  7. If you write files with overwrite=False, an existing file is now refused instead of replaced. (#668)
  8. 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() and execute() are gone from relations. The error names the replacement: limit(1).fetchall(), to_arrow_reader() or limit(n).df() for batches, and materialize() 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 as CREATE TABLE or an INSERT without RETURNING, returns a relation over what the statement returned instead of None: 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, CHECKPOINT and EXPLAIN run at the call and the relation holds their output.

  • Over a join, l.x and r.y resolve 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 are name and name_1 past that point.

  • A persistent view over held rows (materialize(), values(), a statement that ran at the call) is refused; use rel.create("name") or con.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=True or 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, preview or group_by is reached as rel["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 TABLE works.
  • 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) raises TypeError; combine conditions with &, | and ~. An operand no expression can hold raises TypeError where == used to return False.
  • ConstantExpression({1: "a"}) is a MAP; 1.5 made a STRUCT with the key "1".
  • update(set={...}) refuses a bare string value; write col("name") to copy a column or lit("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 memoryview given to values() or an expression is read as a BLOB in logical byte order, where 1.5 raised BufferError. 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...

Read more

v1.5.6 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 28 Sep 13:35
802a345

See DuckDB's changelog for all changes in DuckDB.

v1.5.5 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 22 Jul 10:57
b236c81

See DuckDB's changelog for all changes in DuckDB.

What's Changed in DuckDB-Python

v1.5.4 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 17 Jun 10:51
305369d

See DuckDB's changelog for all changes in DuckDB.

What's Changed in DuckDB-Python

v1.4.5 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 17 Jun 10:51
416cc6e

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

Choose a tag to compare

@evertlammerts evertlammerts released this 20 May 11:59
811b135

See the DuckDB core release notes here: https://github.com/duckdb/duckdb/releases/tag/v1.5.3

What's Changed in DuckDB Python

Full Changelog: v1.5.2...v1.5.3

v1.5.2 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 13 Apr 11:36
2aea44e

See the DuckDB core release notes here: https://github.com/duckdb/duckdb/releases/tag/v1.5.2

What's Changed in duckdb-python

v1.5.1 Bugfix Release

Choose a tag to compare

@evertlammerts evertlammerts released this 23 Mar 12:13
b54a492

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"

Choose a tag to compare

@evertlammerts evertlammerts released this 09 Mar 12:37
87e7928

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.typing and duckdb.functional modules. These were deprecated in 1.4.0. Use duckdb.sqltypes and duckdb.func instead.
  • Renamed column parameter to expression in 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() and fetch_record_batch() on connections and relations. Use the new to_arrow_table() and to_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() and get_profiling_json() methods on connections, plus a refactored query_graph module with improved HTML visualization (dark mode, expandable phases, depth).
  • to_arrow_table() and to_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 using PhysicalArrowCollector for 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_CallObject return 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.dataset import 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

Choose a tag to compare

@evertlammerts evertlammerts released this 26 Jan 12:46
a12f36c

DuckDB core v1.4.4 Changelog: duckdb/duckdb@v1.4.3...v1.4.4

What's Changed in the Python Extension

Full Changelog: v1.4.3...v1.4.4