Skip to content

Python Runtime

Package: xpr-lang · GitHub · v0.5.0

Install

bash
pip install xpr-lang

Requires Python 3.10+. Zero runtime dependencies.

API

Xpr()

Creates a new XPR engine instance.

python
from xpr import Xpr
engine = Xpr()

engine.evaluate(expression, context=None)

Evaluates an XPR expression string against an optional context dict. Returns the result value.

Raises XprError if the expression is invalid or a runtime error occurs.

python
engine.evaluate('1 + 2')
# → 3

engine.evaluate('user["name"] ?? "anonymous"', {'user': {'name': None}})
# → "anonymous"

engine.evaluate('[1,2,3].map(x => x * 2)')
# → [2, 4, 6]

engine.evaluate(
    'items.filter(x => x.active).map(x => x.name)',
    {'items': [{'name': 'a', 'active': True}, {'name': 'b', 'active': False}]}
)
# → ['a']

engine.add_function(name, fn)

Registers a custom function that can be called from expressions.

python
engine.add_function('slugify', lambda s: str(s).lower().replace(' ', '-'))

engine.evaluate('slugify(product.name)', {'product': {'name': 'Hello World'}})
# → 'hello-world'

XprError

Raised on parse errors and runtime errors. Has a position attribute (character offset).

python
from xpr import Xpr, XprError

try:
    engine.evaluate('1 / 0')
except XprError as e:
    print(e)  # "Division by zero"
    print(e.position)  # character offset

Type Notes

  • Numbers are always float internally (e.g. 1 + 2 returns 3.0)
  • null in XPR maps to None in Python
  • Arrays map to list, objects map to dict
  • Arrow functions become Python callables

v0.2 Features

Let Bindings

python
engine.evaluate('let x = 1; let y = x + 1; y')
# → 2.0

engine.evaluate('let f = (x) => x * 2; f(5)')
# → 10.0

engine.evaluate('let items = [1,2,3,4,5]; items.filter(x => x > 2).map(x => x * 10)')
# → [30.0, 40.0, 50.0]

Spread Operator

python
engine.evaluate('[...[1,2], ...[3,4]]')
# → [1.0, 2.0, 3.0, 4.0]

engine.evaluate('{...defaults, ...overrides}', {
    'defaults': {'color': 'blue', 'size': 10},
    'overrides': {'color': 'red'},
})
# → {'color': 'red', 'size': 10.0}

New Array Methods

python
engine.evaluate('[1,2,3].includes(2)')          # → True
engine.evaluate('[1,2,3].indexOf(2)')           # → 1.0
engine.evaluate('[1,2,3,4,5].slice(1, 3)')      # → [2.0, 3.0]
engine.evaluate('[1,2,3].join(", ")')           # → "1, 2, 3"
engine.evaluate('[1,2].concat([3,4])')          # → [1.0, 2.0, 3.0, 4.0]
engine.evaluate('[[1,2],[3,4]].flat()')         # → [1.0, 2.0, 3.0, 4.0]
engine.evaluate('[1,2,1,3].unique()')           # → [1.0, 2.0, 3.0]
engine.evaluate('[1,2,3].zip([4,5,6])')         # → [[1.0,4.0],[2.0,5.0],[3.0,6.0]]
engine.evaluate('[1,2,3,4,5].chunk(2)')         # → [[1.0,2.0],[3.0,4.0],[5.0]]
engine.evaluate('[1,2,3].groupBy(x => x > 1 ? "big" : "small")')
# → {'big': [2.0, 3.0], 'small': [1.0]}

New String Methods

python
engine.evaluate('"hello".indexOf("ll")')        # → 2.0
engine.evaluate('"ab".repeat(3)')               # → "ababab"
engine.evaluate('"  hi  ".trimStart()')         # → "hi  "
engine.evaluate('"  hi  ".trimEnd()')           # → "  hi"
engine.evaluate('"hello".charAt(1)')            # → "e"
engine.evaluate('"42".padStart(5, "0")')        # → "00042"
engine.evaluate('"hi".padEnd(5, ".")')          # → "hi..."

New Object Methods

python
engine.evaluate('{"b": 2, "a": 1}.entries()')  # → [["a", 1.0], ["b", 2.0]]
engine.evaluate('{"a": 1}.has("a")')            # → True
engine.evaluate('{"a": 1}.has("b")')            # → False

range() Function

python
engine.evaluate('range(5)')           # → [0.0, 1.0, 2.0, 3.0, 4.0]
engine.evaluate('range(1, 5)')        # → [1.0, 2.0, 3.0, 4.0]
engine.evaluate('range(0, 10, 2)')    # → [0.0, 2.0, 4.0, 6.0, 8.0]
engine.evaluate('range(5, 0, -1)')    # → [5.0, 4.0, 3.0, 2.0, 1.0]

v0.3 Features

Date/Time

Dates are epoch milliseconds (UTC only). Numbers return as float.

python
engine.evaluate('formatDate(now(), "yyyy-MM-dd")')
# → "2026-03-15"

engine.evaluate('dateDiff(parseDate("2024-01-01T00:00:00Z"), now(), "days")')
# → 439.0

engine.evaluate('dateAdd(parseDate("2024-01-31T00:00:00Z"), 1, "months")')
# → 1709337600000.0

Regex Functions

python
engine.evaluate('matches("hello 42", "\\\\d+")')               # → True
engine.evaluate('match("order-123", "\\\\d+")')                 # → "123"
engine.evaluate('matchAll("a1b2c3", "\\\\d")')                  # → ["1", "2", "3"]
engine.evaluate('replacePattern("2024-01-15","(\\\\d{4})-(\\\\d{2})-(\\\\d{2})","$3/$2/$1")')
# → "15/01/2024"

Negative Indexing and Spread in Calls

python
engine.evaluate('[1,2,3][-1]')           # → 3.0
engine.evaluate('max(...[1, 5, 3, 2])')  # → 5.0

v0.4 Features

Destructuring

python
engine.evaluate('let {name, age} = user; name', {'user': {'name': 'Alice', 'age': 30}})
# → 'Alice'

engine.evaluate('users.map(({name, age}) => `${name}: ${age}`)',
    {'users': [{'name': 'Alice', 'age': 30}]})
# → ['Alice: 30']

engine.evaluate('let [head, ...tail] = items; tail', {'items': [1, 2, 3]})
# → [2.0, 3.0]

Regex Literals

python
engine.evaluate('/\\\\d+/.test("order-123")')      # → True
engine.evaluate('"2024-01-15".match(/\\\\d{4}/)')  # → "2024"
engine.evaluate('"hello world".replace(/o/, "0")')  # → "hell0 w0rld"

v0.5 Features

Math Functions and Constants

python
engine.evaluate('sqrt(16)')        # → 4.0
engine.evaluate('log(E)')          # → 1.0
engine.evaluate('pow(2, 10)')      # → 1024.0
engine.evaluate('PI * pow(5, 2)')  # → 78.53981633974483
engine.evaluate('sign(-7)')        # → -1.0
engine.evaluate('trunc(3.9)')      # → 3.0

Type Predicates

python
engine.evaluate('isNumber(42)')       # → True
engine.evaluate('isString("x")')      # → True
engine.evaluate('isArray([1,2])')     # → True
engine.evaluate('isObject([1,2])')    # → False (arrays are "array" type)
engine.evaluate('isNull(null)')       # → True

New Array Methods

python
engine.evaluate('[3,null,1,null,5].compact().sortBy(x => x)')   # → [1.0, 3.0, 5.0]
engine.evaluate('[1,2,3,4,5].take(3)')                           # → [1.0, 2.0, 3.0]
engine.evaluate('[1,2,3,4].sum()')                               # → 10.0
engine.evaluate('[1,2,3,4].avg()')                               # → 2.5
engine.evaluate('[1,2,3,4].partition(x => x > 2)')               # → [[3.0, 4.0], [1.0, 2.0]]
engine.evaluate('[3,1,2].first()')                               # → 3.0
engine.evaluate('[1,2,3].count(x => x > 1)')                    # → 2.0

fromEntries, str.split(/regex/), Rest Parameters

python
engine.evaluate('fromEntries([["a", 1], ["b", 2]])')  # → {'a': 1.0, 'b': 2.0}
engine.evaluate('"a1b2c3".split(/\\\\d+/)')             # → ['a', 'b', 'c']