Featured image of post DuckDB + Python Toolkit: Seamless Integration with pandas, Polars, and Arrow

DuckDB + Python Toolkit: Seamless Integration with pandas, Polars, and Arrow

Master DuckDB's zero-copy integration with pandas, Polars, and Apache Arrow. Learn 5 practical techniques for handling large files, multi-format reads, and building production data pipelines.

DuckDB + Python Toolkit: Seamless Integration with pandas, Polars, and Arrow

Why You Need a Unified Python Data Processing Toolkit

In daily Python data analysis, we almost always work with three libraries: pandas, Polars, and Apache Arrow. Pandas is the de facto standard, Polars has risen thanks to its speed, and Arrow is the industry norm for columnar memory format.

But when data volume grows, problems emerge:

# The classic pandas dilemma
df = pd.read_csv('big_file.csv')      # 10GB file → Out of Memory
df = df[df['amount'] > 0]              # Must load everything before filtering
result = df.groupby('category').sum()  # Aggregation is painfully slow

DuckDB changes this picture. It’s not a database server you need to deploy—it’s an analytical engine that embeds directly into your Python process. With zero-copy integration alongside pandas, Polars, and Arrow, you can achieve order-of-magnitude performance gains without changing your workflow.

This article systematically covers 5 most practical DuckDB + Python integration techniques based on our latest channel push.


Technique 1: Zero-Copy pandas ↔ DuckDB Conversion

Many users don’t know that DuckDB can directly read pandas DataFrames without writing to CSV first and reading back. The entire process keeps data in memory with no serialization overhead.

Register a DataFrame as a Temporary Table

import duckdb
import pandas as pd

# Assume you already have a pandas DataFrame
df = pd.read_parquet('sales.parquet')

# Register directly as a table — zero copy
con = duckdb.connect()
con.register('sales', df)

# Process with SQL, convert results back to DataFrame
result = con.execute("""
    SELECT 
        DATE_TRUNC('month', sale_date) AS month,
        SUM(amount) AS total,
        COUNT(*) AS orders
    FROM sales
    WHERE status = 'completed'
    GROUP BY 1
    ORDER BY 1
""").fetchdf()

print(result)

Key Points

OperationDescription
con.register('name', dataframe)Registers a DataFrame as a DuckDB temporary table
con.execute(...).fetchdf()Query results become a pandas DataFrame directly
Data transferZero-copy via Arrow format, no serialization overhead

The reverse also works—DuckDB query results can be converted directly to pandas:

# Query Parquet files from S3 in one step
df_result = con.execute("""
    SELECT * FROM read_parquet('s3://bucket/data/*.parquet')
    WHERE date >= '2026-01-01'
""").fetchdf()

Technique 2: Polars Users — Arrow is the Bridge

Polars is built on Apache Arrow, and DuckDB natively supports Arrow. This means data can be passed between them with zero copy.

import duckdb
import polars as pl

# Polars DataFrame → DuckDB (via Arrow, zero copy)
pl_df = pl.read_parquet('sales.parquet')
con = duckdb.connect()

# Method A: Pass Arrow table directly
arrow_table = pl_df.to_arrow()
con.register('sales_arrow', arrow_table)

result = con.execute("SELECT category, AVG(amount) FROM sales_arrow GROUP BY 1").fetchdf()

# Method B: Convert DuckDB results directly back to Polars
duckdb_result = con.execute("""
    SELECT category, SUM(amount) AS total_sales
    FROM sales_arrow
    GROUP BY 1
    HAVING total_sales > 10000
    ORDER BY total_sales DESC
""").fetch_arrow_table()

polars_result = pl.from_arrow(duckdb_result)
print(polars_result)

Why This Matters

  • Polars excels at columnar calculations, ideal for fast per-column transformations and filtering
  • DuckDB excels at complex SQL queries, window functions, CTEs, and multi-table joins
  • They connect through Arrow, so data doesn’t need to be copied—zero performance loss

Use Polars for quick exploration and DuckDB for heavy lifting—that’s the true best combination.


Technique 3: Large File Processing — Never Load Everything into Memory

This is DuckDB’s killer use case. You have a 10GB CSV or Parquet file that pandas simply can’t handle?

import duckdb

con = duckdb.connect()

# Query large files on disk directly, return only what you need
top_categories = con.execute("""
    SELECT 
        category,
        SUM(amount) AS total_sales,
        COUNT(*) AS order_count,
        AVG(amount) AS avg_order_value
    FROM read_csv_auto('/data/sales_2026.csv', header=true)
    GROUP BY category
    ORDER BY total_sales DESC
    LIMIT 10
""").fetchdf()

print(top_categories)

Compare with the pandas approach:

# ❌ Pandas: Load the entire file into memory first
import pandas as pd
df = pd.read_csv('/data/sales_2026.csv')  # 10GB file → OOM
result = df.groupby('category').agg(
    total_sales=('amount', 'sum'),
    order_count=('amount', 'count'),
    avg_order_value=('amount', 'mean')
).nlargest(10, 'total_sales')

Core Advantages

DimensionpandasDuckDB
Memory usageFull file loadedOnly reads needed columns and data
10GB file actual read10GB+Possibly just 200MB
Predicate pushdown❌ Not supported✅ Automatic optimization
Columnar scanning❌ Loads all columns✅ Reads only required columns

DuckDB uses predicate pushdown and columnar scanning techniques. For a 10GB file, it may only need to read 200MB of data. Memory usage drops from 10GB+ to tens of MB. This isn’t optimization—it’s an architectural generational gap.


Technique 4: One-Click Multi-Format File Reading

DuckDB can read various formats directly without prior conversion. This is extremely practical in daily work—your boss sends an Excel file, a colleague drops a JSON on you, historical data is still in SQLite.

import duckdb

con = duckdb.connect()

# CSV
con.execute("SELECT * FROM read_csv_auto('data.csv')")

# Excel
con.execute("SELECT * FROM read_xlsx('data.xlsx')")

# JSON
con.execute("SELECT * FROM read_json_auto('data.json')")

# Parquet
con.execute("SELECT * FROM read_parquet('data.parquet')")

# Multiple Parquet files auto-merged
con.execute("SELECT * FROM read_parquet('s3://bucket/data/*.parquet')")

# Directly query SQLite database
con.execute("SELECT * FROM sqlite_scan('legacy.db', 'users')")

# Remote MySQL/PostgreSQL queries
con.execute("SELECT * FROM mysql_scan('host=localhost', 'database', 'users')")
con.execute("SELECT * FROM postgres_scan('dbname=sales host=localhost', 'orders')")

Practical Example: Turn Excel Reports into SQL Query Objects

import duckdb

con = duckdb.connect()

# Your boss sends an Excel file with 5 worksheets
sheets = con.execute("""
    SELECT sheet_name 
    FROM read_xlsx_auto('monthly_report.xlsx')
""").fetchall()

# Query a specific worksheet
jan_data = con.execute("""
    SELECT * 
    FROM read_xlsx('monthly_report.xlsx', sheet='January')
""").fetchdf()

# Merge multiple months
feb_data = con.execute("""
    SELECT * 
    FROM read_xlsx('monthly_report.xlsx', sheet='February')
""").fetchdf()

combined = jan_data.vstack(feb_data) if hasattr(jan_data, 'vstack') else pd.concat([jan_data, feb_data])

Technique 5: DuckDB as the “Acceleration Engine” for pandas/Polars

You don’t have to abandon pandas or Polars. DuckDB can serve as their backend engine, making slow operations fast.

Replace Slow groupby with SQL

import duckdb
import pandas as pd

# Traditional slow pandas operation
# df.groupby('category').sum() is very slow on large datasets

# Accelerate with DuckDB
con = duckdb.connect()
con.register('df', your_dataframe)

fast_result = con.execute("""
    SELECT 
        category,
        SUM(sales) AS total_sales,
        AVG(sales) AS avg_sales,
        COUNT(DISTINCT user_id) AS unique_users
    FROM df
    GROUP BY category
""").fetchdf()

Hybrid Polars + DuckDB Usage

import duckdb
import polars as pl

con = duckdb.connect()

# Convert Polars DataFrame to Arrow then register
arrow_table = pl_df.to_arrow()
con.register('polars_df', arrow_table)

# Use DuckDB for complex SQL, Polars for column transformations
sql_result = con.execute("""
    WITH monthly AS (
        SELECT 
            DATE_TRUNC('month', date) AS month,
            SUM(amount) AS revenue
        FROM polars_df
        GROUP BY 1
    )
    SELECT 
        month,
        revenue,
        LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
        ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 
              / NULLIF(LAG(revenue) OVER (ORDER BY month), 0), 2) AS growth_pct
    FROM monthly
""").fetch_arrow_table()

final_df = pl.from_arrow(sql_result)

Comparison with Traditional Tools

OperationpandasPolarsDuckDB + Python
Read CSVpd.read_csv()pl.scan_csv()read_csv_auto()
Read Parquetpd.read_parquet()pl.scan_parquet()read_parquet()
Read Excelpd.read_excel()❌ Not supportedread_xlsx()
Read JSONpd.read_json()pl.scan_ndjson()read_json_auto()
DataFrame→TableNot supportedNot supportedcon.register()
Query→DataFrameN/AN/A.fetchdf()
Query→PolarsN/AN/A.fetch_arrow_table()
Large file aggregationgroupby().sum() OOMgroup_by().sum()SQL GROUP BY streaming
Window functionstransform()over()OVER()
Remote data sourcesRequires extra libsRequires extra libsBuilt-in support

Quick Reference: Common Functions

FunctionpandasDuckDB SQL
Read CSVpd.read_csv()read_csv_auto('file.csv')
Read Parquetpd.read_parquet()read_parquet('file.parquet')
Read Excelpd.read_excel()read_xlsx('file.xlsx')
Read JSONpd.read_json()read_json_auto('file.json')
Register DataFrameNot supportedcon.register('name', df)
Result to DataFrameN/A.fetchdf()
Result to PolarsN/A.fetch_arrow_table() + pl.from_arrow()
Group aggregationdf.groupby().agg()SELECT ... GROUP BY
Window functionsdf.transform()LAG()/ROW_NUMBER() OVER()
Date truncationdt.floor()DATE_TRUNC('month', date)

Monetization Suggestions

Mastering the integration of DuckDB + Python ecosystem can generate multiple business value streams:

1. Productize Data Services

Package commonly used data processing workflows into reusable Python packages or API services. Offer small and medium enterprises a one-stop “data cleaning + report generation” solution, charging ¥299-999 per month.

2. Technical Consulting and Training

Many teams are still struggling with pandas on large files, hitting out-of-memory errors regularly. You can provide DuckDB migration consulting for enterprises, helping them重构 their data processing pipelines. Single projects charge ¥5,000-20,000.

3. Automated Reporting SaaS

Build a lightweight BI platform based on DuckDB + Python that supports CSV/Excel/Parquet upload and automatically generates SQL analysis and visualization reports. Charge ¥99-299/month targeting e-commerce and operations teams.

4. Open Source Projects + Paid Value-Added Services

Open-source the utility code from this article to build personal brand. Monetize through GitHub Sponsors, paid tutorials, and priority technical support.

5. Data Engineering Template Marketplace

Package DuckDB + pandas/Polars integration patterns into reusable ETL templates. Sell them on template marketplaces or knowledge payment platforms at ¥49-199 per template.


Summary

The right way to use DuckDB + Python:

  1. pandas users — Use con.register() to turn DataFrames into tables, use SQL to replace slow groupby/merge
  2. Polars users — Pass data zero-copy through Arrow; Polars handles column transforms, DuckDB handles aggregation and window functions
  3. Large file processing — Never pd.read_csv() an entire file; query directly with DuckDB
  4. Multi-format support — CSV, Excel, JSON, Parquet, SQLite, MySQL, PostgreSQL all queryable directly
  5. Hybrid usage — No need to choose one; DuckDB is the acceleration engine for pandas/Polars

Remember one principle: small data with pandas, big data with DuckDB, switch seamlessly with just a few lines of code.

📖 More DuckDB Python实战 tips → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.