datalogic-py

May 11, 2026 · View on GitHub

PyPI License: Apache 2.0

Python bindings for datalogic-rs, a fast Rust implementation of JSONLogic. Same rules, same semantics as the Rust crate, with the compile-once / evaluate-many pattern exposed natively — compile a rule once and evaluate it against thousands of data inputs without re-parsing.

For the cross-runtime overview and the API-tier model every binding implements, see the repo README.

New in v5. datalogic-py is new — there is no v4 Python package. If you were calling the v4 Rust crate or the v4 @goplasmatic/datalogic WASM package, the engine's v4 → v5 changes are catalogued in MIGRATION.md.

Install

pip install datalogic-py

Pre-built wheels are published for:

PlatformArchitectures
Linux (manylinux)x86_64, aarch64
Linux (musllinux)x86_64, aarch64
macOSx86_64, arm64
Windowsx86_64

Python 3.10 and newer are supported via PEP 384 stable ABI (abi3) — one wheel per platform covers every CPython 3.10+ release.

Naming: pip install datalogic-py (PyPI distribution name) → import datalogic_py (Python module name). Python modules can't contain hyphens, so the underscore form is the import.

Quick start

from datalogic_py import apply

result = apply(
    {"if": [{">": [{"var": "score"}, 50]}, "pass", "fail"]},
    {"score": 75},
)
# -> "pass"

API reference

The Python binding mirrors the Rust engine's API tier model.

TierEntry pointUse when
One-shotapply(rule, data)Ad-hoc evaluation, one rule + one data shape
EngineEngine().eval(rule, data)Custom configuration (templating, future operator extensions)
Compile onceEngine().compile(rule).evaluate(data)Same rule evaluated against many data inputs
Sessionwith engine.session() as sess: …Hot loops — amortise arena reset across iterations

One-shot — apply(rule, data)

from datalogic_py import apply

apply({"+": [1, 2, 3]}, {})                                # 6
apply({"var": "user.age"}, {"user": {"age": 25}})          # 25
apply({"and": [{">": [{"var": "x"}, 0]}, True]}, {"x": 5}) # True

Both arguments accept Python dict / list values (converted via pythonize, roughly 3–10× faster than a JSON-string round-trip). For payloads with types pythonize doesn't cover, see Type conversion below.

Engine — Engine().eval(rule, data)

Construct an Engine when you need templating mode or any non-default configuration:

from datalogic_py import Engine

engine = Engine()                          # default config
engine.eval({"==": [1, 1]}, {})            # True

# Templating mode — multi-key objects become output templates
templating_engine = Engine(templating=True)
templating_engine.eval(
    {"name": {"var": "user.name"}, "ok": {">": [{"var": "score"}, 50]}},
    {"user": {"name": "Ada"}, "score": 99},
)
# {"name": "Ada", "ok": True}

Compile once — Engine().compile(rule)Rule.evaluate(data)

Compile the rule once when you'll evaluate it against many data inputs.

from datalogic_py import Engine

engine = Engine()
rule = engine.compile({"if": [{">": [{"var": "score"}, 50]}, "pass", "fail"]})

for payload in batch:
    result = rule.evaluate(payload)         # accepts a dict
    fast   = rule.evaluate_str(json_text)   # accepts a JSON string (skips dict conversion)

Rule is thread-safe — clone the reference into worker threads and evaluate concurrently. The Rust eval call releases the GIL, so a multi-threaded server gains real parallelism.

Session — hot loops

For batches where you want to amortise arena reset across iterations, open a Session. The arena is reset between iterations automatically.

from datalogic_py import Engine

engine = Engine()
rule = engine.compile({"+": [{"var": "x"}, 1]})

with engine.session() as sess:
    for payload in batch:
        result = sess.evaluate(rule, payload)

Session is the per-thread workhorse — open one per worker thread. The arena that makes it fast can't be shared across threads (the same way a database connection is per-task in a connection-pool model); Engine and Rule are both thread-safe, so share those.

Error handling

All exceptions descend from DataLogicError:

ExceptionWhen
ParseErrorMalformed rule or data JSON, unsupported operator, or unsupported Python type
EvaluateErrorOperator failure at runtime — carries .error_type, .operator, .path
from datalogic_py import Engine, EvaluateError

engine = Engine()
try:
    engine.eval({"var": "missing"}, {})
except EvaluateError as e:
    print(e.error_type)  # e.g. "VariableNotFound"
    print(e.operator)    # "var"
    print(e.path)        # JSON-pointer-style path through the compiled tree

Threading

TypePattern
EngineBuild once; share across threads
RuleCompile once; share across threads — evaluate releases the GIL for parallelism
SessionOne per worker thread — the per-task workhorse

Type conversion

The dict-input path uses pythonize:

Supported: dict, list, str, int, float, bool, None.

Not supported — these raise ParseError with a clear message:

  • datetime.datetime, datetime.date — convert to ISO string at the Python edge
  • decimal.Decimal — convert to float or str
  • bytes, set, tuple
  • float('nan'), float('inf') — JSON spec disallows them

For payloads with exotic types, use rule.evaluate_str(json_text) and bring your own JSON encoder (e.g. with default=str).

Templating mode

engine = Engine(templating=True)
rule = engine.compile({
    "name": {"var": "user.name"},
    "ok": {">": [{"var": "score"}, 50]},
})
rule.evaluate({"user": {"name": "Ada"}, "score": 99})
# -> {"name": "Ada", "ok": True}

Performance

This package wraps the same Rust engine measured as dlrs:engine in the cross-library benchmark — geomean 9.7 ns/op across 44 operator suites in native Rust. The pyo3 boundary and pythonize dict conversion add a small per-call cost on top; use rule.evaluate_str(json_text) when you already have a JSON string and want to skip the dict path. evaluate releases the GIL, so a multi-threaded server gains real parallelism on top of the engine's native speed.

Learn more

License

Apache-2.0. See the main repository for source and contribution guidelines.