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.10.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.10.0
File Size Uploaded
dpyr-1.10.0.tar.gz 164.1 kB Details

Built distribution (wheel)

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

Total release size: 232.7 kB

Release files / dpyr-1.10.0.tar.gz

Download URL dpyr-1.10.0.tar.gz
Size 164.1 kB
Tags Source
SHA-256 checksum
How to use checksums
43864a0f4ca19c727a58b7561bfeef0ca386c402ecfce0eb01182ad3ae43eec6
BLAKE2b-256 checksum
How to use checksums
ff0ee0ca74a353798cc447dc1c85779f20da86a10fb231df7bf63049bb89a3b3
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 26, 2026.

Transparency log

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

Download URL dpyr-1.10.0-py3-none-any.whl
Size 68.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d9a65f019386c6df58d3ab55a8bbd84c9c9c7c9b1dfa0e9efefb0702da60ebfb
BLAKE2b-256 checksum
How to use checksums
6b4ba877b63f9d3854d0e82bd136e67a3488f67ac12ed99aa7e5a49ada1a290a
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 26, 2026.

Transparency log

Release history Release notifications | RSS feed

1.11.0

2 release files

1.10.1

2 release files

This release

1.10.0 This release

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