Skip to main content

dpyr

dplyr for Python. The tidyverse's verbs — filter, mutate, group_by, summarize, joins, across, tidyselect — as Python method chains, executing on polars or duckdb, with real IDE autocompletion and semantics verified against dplyr itself.

pip install dpyr        # or: uv add dpyr
from dpyr import read, col, n, desc

starwars = read("starwars.parquet")   # read() takes anything tabular:
                                      # .parquet/.csv/.arrow/.db paths, dicts,
                                      # lists of records, polars/pandas frames,
                                      # arrow tables, Hugging Face datasets,
                                      # numpy/torch/jax

(
    starwars
    .filter(col.height > 180, col.mass < 100)
    .mutate(bmi = col.mass / (col.height / 100) ** 2)
    .group_by(col.species)
    .summarize(
        n = n(),
        mean_bmi = col.bmi.mean(),
    )
    .arrange(desc(col.mean_bmi))
)

Evaluate that in a notebook and you see rows immediately. Typo a column name and you get the error on that line, with a did-you-mean suggestion. Wrap the same code in a pipeline and only .collect() at the end, and the whole chain runs as one fused query with predicate pushdown. That combination — schema-eager, data-lazy, display-eager — is the core design.

Two backends, one semantics

import duckdb
from dpyr import read

df  = read({"x": [1, 2, 3], "g": ["a", "a", "b"]})   # polars engine
con = duckdb.connect("warehouse.db")
tbl = read(con, "events")                            # SQL pushdown

Identical chains produce identical results on both engines — enforced by a Hypothesis fuzzer that runs random verb chains on both and compares bit-for-bit, and by differential tests against real dplyr: every spec in tests/specs/ is executed by dplyr (via oracle/run_specs.R) to produce a committed golden parquet, then replayed through dpyr on both backends. Where R and the engines genuinely disagree, the decision is documented in docs/SEMANTICS.md, not left to chance.

The dplyr you know

dplyr dpyr
filter(df, height > 180) df.filter(col.height > 180)
mutate(df, bmi = mass / h^2) df.mutate(bmi = col.mass / col.h ** 2)
summarise(df, n = n(), m = mean(x, na.rm = TRUE)) df.summarize(n = n(), m = col.x.mean())
arrange(df, desc(mass)) df.arrange(desc(col.mass))
select(df, name, starts_with("h")) df.select(col.name, starts_with("h"))
select(df, -mass) df.select(-col.mass)
across(where(is.numeric), mean) across(where(is_numeric), "mean")
left_join(a, b, by = "k") a.left_join(b, on = col.k)
pivot_longer(df, x:y) df.pivot_longer([col.x, col.y])
if_else(), case_when(), n_distinct() if_else(), case_when(), .n_unique()
lag(), lead(), row_number(), min_rank() lag(), lead(), row_number(), min_rank()
cumsum(), dense_rank(), percent_rank() cum_sum(), dense_rank(), percent_rank()
slice_min(x, n), slice_max(x, n) (ties kept) slice_min(col.x, n), slice_max(col.x, n)
separate(), unite(), relocate() separate(), unite(), relocate()
coalesce(), replace_na() coalesce(), replace_na()

Grouped mutate/filter are windowed per group, summarize peels one grouping level, joins use .x/.y suffixes and match NAs by default — the dplyr behaviors, deliberately.

Autocompletion that actually works

  • df.c.height — frame-bound proxy: column names complete from the live schema, and the returned expression is typed (.mean() on numerics, .str_detect() on strings; calling .mean() on a string column raises immediately, at build time).
  • df.filter(lambda c: c.height > 180) — lambda style for the same effect.
  • dpyr stubgen data/*.parquet -o schemas.py — generates typed schema modules so completion and type-checking work statically in any IDE.

The database is a destination, not just a source

db = read("warehouse.db")                 # catalog object: db.tables, db.orders
gold = db.orders.group_by(col.region).summarize(rev = col.amount.sum())
gold.to_table("gold_revenue")             # CREATE TABLE AS <sql>, fully in-engine
gold.to_view("gold_live")                 # the lazy plan as a named view
gold.write("gold.parquet")                # in-engine COPY (extension dispatch)
mem = read({"region": ["east"], "target": [1000.0]})
gold.inner_join(mem, on = col.region)     # in-memory frames bridge into duckdb
                                          # automatically (arrow, zero-copy)

Your own functions, row by row

from dpyr import vectorize

@vectorize(threads=8)
def classify(text: str, labels: str) -> str: ...      # any Python: a parser, an API, a model

reviews.mutate(topic = classify(col.text, labels="price,quality"))

Typed from the annotation, one call per distinct input, results remembered for the session, and a displayed dataframe only runs the rows it shows (see the row functions guide).

Interactive by default, lazy when you need it

df.persist()           # checkpoint: materialize now (duckdb: temp table)
df.lazy()              # this frame never executes implicitly
dpyr.options.interactive = False   # global opt-out for production pipelines

Results are cached by plan hash, so re-displaying a frame in a notebook never recomputes it.

Documentation

Full guides at maximerivest.github.io/dpyr — get started, grouped data, joins, window functions, column-wise operations, reshaping, expressions & autocompletion, and the backends guide (connecting and operating polars and duckdb).

Project documents

Doc What it pins down
docs/DESIGN.md API design, the materialization model, autocompletion strategy, architecture
docs/SEMANTICS.md Every deliberate decision where R, polars and duckdb disagree
docs/TESTING.md dplyr-as-oracle goldens, backend-agreement fuzzing, Hypothesis properties
docs/ROADMAP.md What shipped in 1.0 and what's next

License

MIT © Maxime Rivest

Metadata

Release files for dpyr 1.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dpyr 1.11.0
File Size Uploaded
dpyr-1.11.0.tar.gz 167.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dpyr 1.11.0
File Interpreter ABI Platform
dpyr-1.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 236.5 kB

Release files / dpyr-1.11.0.tar.gz

Download URL dpyr-1.11.0.tar.gz
Size 167.4 kB
Tags Source
SHA-256 checksum
How to use checksums
101497cf02c862b2abc471d501f4ffcb34d3d21af6bc0bc07c2f1ca638d353d2
BLAKE2b-256 checksum
How to use checksums
cd7d39c189e1e3e986d67ac61bbb50e4363093c57d3a8d6bb92ec4168b7a44c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / dpyr-1.11.0-py3-none-any.whl

Download URL dpyr-1.11.0-py3-none-any.whl
Size 69.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5def2ec5142af83abf73e0e49b6e1d518ed930ef3e68cf595a2cc36e2383651b
BLAKE2b-256 checksum
How to use checksums
ac78640c10ddfa95245985495433d899f377b2ebfb0411c0c6cf1dc882a9cb08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.11.0 This release

2 release files

1.10.1

2 release files

1.10.0

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.1

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page