Connection management with DuckCon#
DuckCon centralises DuckDB connection handling so resource management,
extension loading, and configuration changes are explicit. This guide walks
through the core features that power application backends and highlights
patterns the DuckPlus team uses in services, notebooks, and CLI utilities.
Lifecycle management#
DuckCon can be used either as a context manager or by calling
:meth:~duckplus.duckcon.DuckCon.connect/:meth:~duckplus.duckcon.DuckCon.close
explicitly. In both cases the class maintains a single live connection at a
time, guarding against concurrent reuse. The is_open property and explicit
error messages make it trivial to write defensive wrappers.
from duckplus import DuckCon
manager = DuckCon(database="warehouse.duckdb", read_only=True)
with manager as con:
assert con.sql("SELECT 1").fetchone() == (1,)
assert manager.connection is None # closed automatically
try:
with manager:
pass
with manager:
pass
except RuntimeError:
print("Double entry prevented")
Attempting to enter the context manager twice raises a RuntimeError, keeping
connection ownership clear. For CLI tools, wrap the with block in
click.Context or similar frameworks so teardown happens even when commands
raise exceptions.
Extension loading#
Many DuckDB features live behind installable extensions. DuckPlus accepts the
extra_extensions argument to install and load them eagerly. This is
idempotent—extensions that are already installed are simply loaded.
manager = DuckCon(extra_extensions=("excel",))
with manager:
extensions = {info.name: info for info in manager.extensions()}
excel_available = extensions.get("excel")
print(excel_available.loaded)
To add an extension after the connection is open, call
manager.connection.install_extension(...) directly and then re-run your
reader. DuckPlus will surface actionable errors when a download is not possible
(for example, in offline environments), allowing callers to provide fallback
paths. When you need to introspect the current state, use
:meth:DuckCon.extensions <duckplus.duckcon.DuckCon.extensions> to retrieve a
tuple of :class:~duckplus.duckcon.ExtensionInfo records with version, alias,
and installation metadata:
with manager:
for info in manager.extensions():
print(info.name, info.installed, info.loaded)
Because extension installation happens inside the with block, the metadata
always reflects the current connection.
Connection configuration#
The optional config dictionary lets you set DuckDB configuration parameters
before the connection opens. DuckPlus normalises keyword arguments and defers to
DuckDB’s validation logic so the API stays future-proof. Use it to toggle
settings such as default_null_order or threads when running analytical
workloads.
manager = DuckCon(config={"allow_unsigned_extensions": True})
with manager as con:
con.execute("PRAGMA verify_parallelism")
Relationship with relations#
Instances of :class:~duckplus.relation.Relation always reference the
DuckCon that produced them. Helpers such as
:meth:duckplus.relation.Relation.append_parquet validate that the originating
connection is still open before issuing writes, preventing confusing runtime
errors.
Because the relation wrapper stores the manager reference, you can share connections across helpers without dealing with raw DuckDB handles. See the immutable relation helper guide for deeper coverage of the immutable relation APIs.
Registering custom helpers#
Helpers bind directly onto the :class:DuckCon class so every instance exposes
the same fluent surface. Use :meth:DuckCon.register_helper <duckplus.duckcon.DuckCon.register_helper> to attach a callable that receives
the active DuckDBPyConnection; the method wraps it in a bound method for you.
DuckPlus ships file readers (read_csv, read_parquet, read_json) and
extension-backed connectors (read_odbc_query, read_odbc_table,
read_excel) that decorate themselves with
:func:duckplus.io.duckcon_helper at import time.
Call the helpers as methods on the manager or via :meth:DuckCon.apply_helper <duckplus.duckcon.DuckCon.apply_helper> without importing :mod:duckplus.io.
Pass overwrite=True to replace these defaults with a project-specific
implementation. Direct binding keeps helper discovery IDE-friendly while still
offering an escape hatch for environment-specific functionality such as
registering a parquet_scan macro or enabling custom pragmas.
def _apply_pragmas(connection, pragmas):
for key, value in pragmas.items():
connection.execute(f"PRAGMA {key}={value}")
manager.register_helper("set_pragmas", _apply_pragmas)
with manager:
manager.apply_helper("set_pragmas", {"threads": 4, "memory_limit": "4GB"})
Because helpers close over :meth:DuckCon.connection, calling
:meth:DuckCon.apply_helper <duckplus.duckcon.DuckCon.apply_helper> before the
connection opens will raise a clear RuntimeError pointing back to
DuckCon.connection.
Testing patterns#
The immutable design makes it straightforward to test connection-heavy code. In
pytest fixtures, construct a DuckCon once and yield the object rather than
the raw connection—relations created during the test will retain access to the
fixture and keep assertions honest. When you need to simulate offline behaviour,
set extra_extensions=() and monkeypatch connection.install_extension to
raise, then assert that your application emits the fallback messaging you
expect.