Skip to content

[Python][RDF] Enable pure Python callbacks in RDataFrame - #23658

Merged
siliataider merged 4 commits into
root-project:masterfrom
siliataider:py-rdf
Oct 16, 2026
Merged

siliataider merged 4 commits into
root-project:masterfrom
siliataider:py-rdf

Conversation

@siliataider

@siliataider siliataider commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

This Pull request:

Now that #21261 landed, Python callables can be passed to C++ templated functions directly.

We apply this in RDataFrame: from now on, a Python function passed to Define or Filter without a decorator runs as it is in the Python interpreter. RDataFrame no longer compiles it with Numba behind the scenes. You can use any Python code in it, including ROOT classes and their methods, and any Python library:

import ROOT
from particle import Particle

def mass_difference(p: ROOT.Math.PtEtaPhiMVector, pdg: int) -> float:
    return p.M() - Particle.from_pdgid(pdg).mass / 1000  # PDG mass in GeV

df = (
    ROOT.RDataFrame(3)
    .Define("p", "ROOT::Math::PtEtaPhiMVector(40., 0., 0., 90. + rdfentry_)")
    .Define("pdg", "23")  # Z boson
    .Define("dm", mass_difference)  # the parameters `p` and `pdg` are read from the columns with the same names
)

The function needs type annotations: RDataFrame reads them to know the C++ types of the parameters and of the return value. These can be C++ type names, Python types or ROOT classes. A function without annotations raises.

Numba stays available as an opt in, with the same ROOT.Numba.Declare decorator as before:

@ROOT.Numba.Declare(["double"], "double")
def square(x):
    return x * x

df = df.Define("x", "(double) rdfentry_").Define("x2", square, ["x"])

Note

extra_args is now only supported for Numba decorated functions. Plain Python functions can use variables from the surrounding code instead.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Test Results

    24 files      24 suites   4d 0h 58m 24s ⏱️
 3 888 tests  3 886 ✅ 0 💤 2 ❌
83 225 runs  83 223 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit 4035219.

♻️ This comment has been updated with latest results.

@siliataider

siliataider commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Doesn't work out of the box because of compiler-research/cppjit#151

To land the functionality while that feature gets fixed, we will opt for explicit instanciation of Define by building the callback's function pointer signature using its annotations

A Python callable that is not decorated with `ROOT.Numba.Declare` is now passed to `Define` and `Filter` as it is and runs in the Python interpreter, instead of being implicitly JIT compiled with Numba. Its type annotations give the function pointer type used to explicitly instantiate `Define` and `Filter`.

Assisted-by: Claude Opus 5.5
…are twice

`FunctionJitter` declared again the callables already declared with `ROOT.Numba.Declare`, which failed with a redefinition error, so passing a decorated callable with `extra_args` to `Define` or `Filter` did not work.

Assisted-by: Claude Opus 5.5
@siliataider
siliataider marked this pull request as ready for review October 13, 2026 17:31

@vepadulano vepadulano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A great improvement in ergonomics for Python users, thanks!

@siliataider
siliataider merged commit e940194 into root-project:master Oct 16, 2026
33 of 36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants