DuckDB Python
Produce explicit, injection-safe DuckDB integrations whose connection,
transaction, relation, input registration, result representation, and execution
boundary match the host application.
Boundary
Use this skill when Python embeds DuckDB or invokes its Python client. SQL
semantics are in scope only for a DuckDB execution target. Do not introduce
DuckDB into a library-neutral dataframe task, and do not apply this skill to a
remote database just because its SQL resembles DuckDB.
Know the runtime objects
| Object |
Meaning |
Use it for |
| Database |
An in-memory catalog/storage instance or a persistent database file. |
Choosing persistence, access mode, and process boundaries. |
DuckDBPyConnection |
A session with catalog, transaction, configuration, registered objects, and current result state. |
All package/library query work; own its lifetime explicitly. |
DuckDBPyRelation |
A symbolic relational query associated with a connection. |
Composable query construction before a fetch, display, write, or create boundary. |
| DB-API result on a connection |
The current executed result consumed by fetch* methods. |
Immediate parameterized statements and bounded result extraction. |
| Catalog table/view |
Durable table or named logical query in the database. |
Reuse across statements and connections according to persistence. |
| Registered Python object |
A connection-scoped view over Arrow/dataframe input. |
Explicitly exposing an in-memory object to SQL while preserving its lifetime. |
| Arrow reader/table or dataframe result |
Materialized or batched data crossing out of DuckDB. |
Matching the downstream consumer without unnecessary conversion. |
The module-level duckdb.sql() family uses a shared global connection. A
relation is lazy in the sense that it represents a query until a fetch, display,
write, or catalog-producing action, but not every relation construction is free
of metadata work. A cursor created from a connection is another handle on the
same connection: it can be thread-local, but queries through sibling handles
serialize instead of running concurrently. Read the connection
and relation model before changing lifetimes,
transactions, registration, or concurrency.
Ordered workflow
- Recover the contract: database location, read/write mode, caller-owned or
callee-owned connection, transaction boundary, input sources, result object,
expected cardinality/order, and memory limit.
- In reusable code, accept or create an explicit connection. Avoid module-level
query state. For parallel query execution, give each worker an independent
connection; sharing one explicit connection or its cursor handles serializes
queries and must be an intentional policy.
- Parameterize values. For identifiers or SQL structure, use a fixed mapping or
validated AST/building path; placeholders do not quote identifiers.
- Choose SQL or the relational API for clarity, then keep one connection and
one deferred query until the required output boundary.
- Push filters and projections into file/table scans; avoid materializing an
input dataframe or full query result merely to continue processing.
- Choose
fetchone/fetchall, Arrow batches/table, pandas, Polars, NumPy, file
output, table, or view from the consumer contract.
- Test values, types, nulls, cardinality, deterministic ordering, transaction
behavior, and cleanup. Inspect
EXPLAIN only for a plan claim.
Choose by intent
| Need |
Choose |
Rule |
| Isolated package operation |
Explicit duckdb.connect(...) or injected connection |
Close only connections you own. |
| Several statements that must commit together |
Explicit transaction |
Roll back on failure; do not rely on incidental autocommit boundaries. |
| Values from untrusted input |
DB-API/connection parameters |
Never interpolate or concatenate them into SQL. |
| Dynamic column/table choice |
Allowlisted mapping to known SQL fragments |
A value placeholder cannot represent SQL identifiers. |
| Composable read-only query |
DuckDBPyRelation or one SQL statement |
Preserve deferral until the consumer boundary. |
| Large Arrow-capable consumer |
Record-batch reader/fetch API |
Do not fetch a full dataframe first. |
| Persistent result |
CREATE TABLE, INSERT, or COPY under an explicit transaction/overwrite policy |
Verify the reopened artifact or catalog object. |
| Temporary query over Python data |
Explicit register/unregister |
Keep the source alive and avoid accidental name capture. |
| File-backed analytics |
Direct read_parquet/read_csv table function or relation |
Keep projection/filter in DuckDB for pushdown. |
Read the operation map for connection, query,
ingestion, export, extension, and plan choices.
Canonical package boundary
from collections.abc import Sequence
from pathlib import Path
import duckdb
SUMMARY_SQL = """
SELECT customer_id, sum(amount) AS total_amount
FROM read_parquet(?)
WHERE amount >= ?
GROUP BY customer_id
ORDER BY total_amount DESC, customer_id ASC
"""
def customer_totals(
parquet_path: Path,
minimum_amount: float,
) -> Sequence[tuple[object, ...]]:
with duckdb.connect(database=":memory:") as connection:
return connection.execute(
SUMMARY_SQL,
[str(parquet_path), minimum_amount],
).fetchall()
The function owns and closes its connection, treats path and threshold as
values, and defines result ordering including a tie-breaker. If the result is
not proven small, change the public return contract to a batched Arrow reader or
write boundary; do not retain fetchall() and hope memory is sufficient.
High-risk rules
Connections, transactions, and threads
- Use explicit connection objects in libraries. Module-level calls share hidden
state and can collide across packages or threads.
- One explicit connection is thread-safe in the current DB-API, but it locks for
each query. Its
.cursor() handles share that connection and therefore
serialize. Give each worker an independent connection when actual parallel
execution is required; use thread-local cursors only when shared-database,
serialized access is the intended contract.
- State who owns each connection. A helper must not close an injected
connection and must not return a relation tied to a connection it just closed.
- Wrap dependent writes in an explicit transaction and test rollback. Do not
mix irreversible external file effects into a database rollback claim.
- Configure access mode, filesystem/network policy, memory, threads, and
extension behavior at the intended scope rather than mutating a global
default invisibly.
SQL, names, and Python inputs
- Bind data values through parameters. Validate dynamic identifiers against a
closed mapping; never use f-strings for untrusted SQL.
- Replacement scans can resolve Python variable names from scope. Prefer
explicit registration in reusable code so dependencies and lifetime are
visible, then unregister in a
finally block when the connection continues.
- A registered dataframe/Arrow object is not automatically copied into a
durable table. Keep its owner alive until the query is consumed.
- Never load unsigned or remote extensions merely to satisfy a prompt. Require
explicit trust and environment policy; pin or inspect extension availability.
Results and performance
- A relation or executed query becomes useful only at a consumer boundary.
Choose the narrowest representation the consumer accepts and avoid dataframe
round trips between DuckDB operations.
- SQL result order is contractual only with
ORDER BY. Add stable tie-breakers
when exact order matters.
- DuckDB can push projections and filters into Parquet scans, but prove the
specific plan with
EXPLAIN or profiling when performance is a requirement.
CREATE TABLE AS copies data into DuckDB; a view over an external scan does
not. Choose persistence intentionally and test reopening when required.
- Schema unification across files can hide drift or insert nulls. Enable it only
under an explicit union contract and test missing, extra, reordered, and
incompatible columns.
Run the installed-API inspector and read version and API
grounding. Test with the DuckDB verification
matrix.
Completion gate
Do not declare completion until connection ownership and cleanup are tested;
values are parameterized and dynamic identifiers allowlisted; required writes
are atomic at the declared boundary; returned relations/readers do not outlive
their connection or source; result columns, DuckDB types, null behavior,
cardinality, and explicit order match the contract; large-result handling is
bounded; persisted output is reopened; and plan/performance claims have direct
evidence. Report unavailable extensions, skipped integration checks, and their
consequences.
References
- Query and result recipes
- Resource and persistence recipes
- Connection and relation model
- Operation map
- Version and API grounding
- Testing DuckDB integrations
1---2name: duckdb-python3description: Use for writing, reviewing, debugging, testing, or optimizing Python code that embeds DuckDB, executes analytical SQL, manages DuckDB connections and transactions, builds DuckDB relations, queries Parquet/CSV/Arrow/pandas/Polars inputs, or exports query results. Trigger on connection scope, parameters, replacement scans, materialization, concurrency, extensions, and query plans. Do not use for generic SQL with another engine, DuckDB CLI-only work, server-database administration, dbt-only projects, or dataframe work that does not call DuckDB.4---56# DuckDB Python78Produce explicit, injection-safe DuckDB integrations whose connection,9transaction, relation, input registration, result representation, and execution10boundary match the host application.1112## Boundary1314Use this skill when Python embeds DuckDB or invokes its Python client. SQL15semantics are in scope only for a DuckDB execution target. Do not introduce16DuckDB into a library-neutral dataframe task, and do not apply this skill to a17remote database just because its SQL resembles DuckDB.1819## Know the runtime objects2021| Object | Meaning | Use it for |22|---|---|---|23| Database | An in-memory catalog/storage instance or a persistent database file. | Choosing persistence, access mode, and process boundaries. |24| `DuckDBPyConnection` | A session with catalog, transaction, configuration, registered objects, and current result state. | All package/library query work; own its lifetime explicitly. |25| `DuckDBPyRelation` | A symbolic relational query associated with a connection. | Composable query construction before a fetch, display, write, or create boundary. |26| DB-API result on a connection | The current executed result consumed by `fetch*` methods. | Immediate parameterized statements and bounded result extraction. |27| Catalog table/view | Durable table or named logical query in the database. | Reuse across statements and connections according to persistence. |28| Registered Python object | A connection-scoped view over Arrow/dataframe input. | Explicitly exposing an in-memory object to SQL while preserving its lifetime. |29| Arrow reader/table or dataframe result | Materialized or batched data crossing out of DuckDB. | Matching the downstream consumer without unnecessary conversion. |3031The module-level `duckdb.sql()` family uses a shared global connection. A32relation is lazy in the sense that it represents a query until a fetch, display,33write, or catalog-producing action, but not every relation construction is free34of metadata work. A cursor created from a connection is another handle on the35same connection: it can be thread-local, but queries through sibling handles36serialize instead of running concurrently. Read [the connection37and relation model](references/object-model.md) before changing lifetimes,38transactions, registration, or concurrency.3940## Ordered workflow41421. Recover the contract: database location, read/write mode, caller-owned or43 callee-owned connection, transaction boundary, input sources, result object,44 expected cardinality/order, and memory limit.452. In reusable code, accept or create an explicit connection. Avoid module-level46 query state. For parallel query execution, give each worker an independent47 connection; sharing one explicit connection or its cursor handles serializes48 queries and must be an intentional policy.493. Parameterize values. For identifiers or SQL structure, use a fixed mapping or50 validated AST/building path; placeholders do not quote identifiers.514. Choose SQL or the relational API for clarity, then keep one connection and52 one deferred query until the required output boundary.535. Push filters and projections into file/table scans; avoid materializing an54 input dataframe or full query result merely to continue processing.556. Choose `fetchone`/`fetchall`, Arrow batches/table, pandas, Polars, NumPy, file56 output, table, or view from the consumer contract.577. Test values, types, nulls, cardinality, deterministic ordering, transaction58 behavior, and cleanup. Inspect `EXPLAIN` only for a plan claim.5960## Choose by intent6162| Need | Choose | Rule |63|---|---|---|64| Isolated package operation | Explicit `duckdb.connect(...)` or injected connection | Close only connections you own. |65| Several statements that must commit together | Explicit transaction | Roll back on failure; do not rely on incidental autocommit boundaries. |66| Values from untrusted input | DB-API/connection parameters | Never interpolate or concatenate them into SQL. |67| Dynamic column/table choice | Allowlisted mapping to known SQL fragments | A value placeholder cannot represent SQL identifiers. |68| Composable read-only query | `DuckDBPyRelation` or one SQL statement | Preserve deferral until the consumer boundary. |69| Large Arrow-capable consumer | Record-batch reader/fetch API | Do not fetch a full dataframe first. |70| Persistent result | `CREATE TABLE`, `INSERT`, or `COPY` under an explicit transaction/overwrite policy | Verify the reopened artifact or catalog object. |71| Temporary query over Python data | Explicit `register`/`unregister` | Keep the source alive and avoid accidental name capture. |72| File-backed analytics | Direct `read_parquet`/`read_csv` table function or relation | Keep projection/filter in DuckDB for pushdown. |7374Read [the operation map](references/operations.md) for connection, query,75ingestion, export, extension, and plan choices.7677## Canonical package boundary7879```python80from collections.abc import Sequence81from pathlib import Path8283import duckdb848586SUMMARY_SQL = """87 SELECT customer_id, sum(amount) AS total_amount88 FROM read_parquet(?)89 WHERE amount >= ?90 GROUP BY customer_id91 ORDER BY total_amount DESC, customer_id ASC92"""939495def customer_totals(96 parquet_path: Path,97 minimum_amount: float,98) -> Sequence[tuple[object, ...]]:99 with duckdb.connect(database=":memory:") as connection:100 return connection.execute(101 SUMMARY_SQL,102 [str(parquet_path), minimum_amount],103 ).fetchall()104```105106The function owns and closes its connection, treats path and threshold as107values, and defines result ordering including a tie-breaker. If the result is108not proven small, change the public return contract to a batched Arrow reader or109write boundary; do not retain `fetchall()` and hope memory is sufficient.110111## High-risk rules112113### Connections, transactions, and threads114115- Use explicit connection objects in libraries. Module-level calls share hidden116 state and can collide across packages or threads.117- One explicit connection is thread-safe in the current DB-API, but it locks for118 each query. Its `.cursor()` handles share that connection and therefore119 serialize. Give each worker an independent connection when actual parallel120 execution is required; use thread-local cursors only when shared-database,121 serialized access is the intended contract.122- State who owns each connection. A helper must not close an injected123 connection and must not return a relation tied to a connection it just closed.124- Wrap dependent writes in an explicit transaction and test rollback. Do not125 mix irreversible external file effects into a database rollback claim.126- Configure access mode, filesystem/network policy, memory, threads, and127 extension behavior at the intended scope rather than mutating a global128 default invisibly.129130### SQL, names, and Python inputs131132- Bind data values through parameters. Validate dynamic identifiers against a133 closed mapping; never use f-strings for untrusted SQL.134- Replacement scans can resolve Python variable names from scope. Prefer135 explicit registration in reusable code so dependencies and lifetime are136 visible, then unregister in a `finally` block when the connection continues.137- A registered dataframe/Arrow object is not automatically copied into a138 durable table. Keep its owner alive until the query is consumed.139- Never load unsigned or remote extensions merely to satisfy a prompt. Require140 explicit trust and environment policy; pin or inspect extension availability.141142### Results and performance143144- A relation or executed query becomes useful only at a consumer boundary.145 Choose the narrowest representation the consumer accepts and avoid dataframe146 round trips between DuckDB operations.147- SQL result order is contractual only with `ORDER BY`. Add stable tie-breakers148 when exact order matters.149- DuckDB can push projections and filters into Parquet scans, but prove the150 specific plan with `EXPLAIN` or profiling when performance is a requirement.151- `CREATE TABLE AS` copies data into DuckDB; a view over an external scan does152 not. Choose persistence intentionally and test reopening when required.153- Schema unification across files can hide drift or insert nulls. Enable it only154 under an explicit union contract and test missing, extra, reordered, and155 incompatible columns.156157Run [the installed-API inspector](scripts/inspect_duckdb.py) and read [version and API158grounding](references/version-grounding.md). Test with [the DuckDB verification159matrix](references/testing.md).160161## Completion gate162163Do not declare completion until connection ownership and cleanup are tested;164values are parameterized and dynamic identifiers allowlisted; required writes165are atomic at the declared boundary; returned relations/readers do not outlive166their connection or source; result columns, DuckDB types, null behavior,167cardinality, and explicit order match the contract; large-result handling is168bounded; persisted output is reopened; and plan/performance claims have direct169evidence. Report unavailable extensions, skipped integration checks, and their170consequences.171172## References173174- [Query and result recipes](references/recipes-query.md)175- [Resource and persistence recipes](references/recipes-resources.md)176- [Connection and relation model](references/object-model.md)177- [Operation map](references/operations.md)178- [Version and API grounding](references/version-grounding.md)179- [Testing DuckDB integrations](references/testing.md)