Mathematical expression evaluation library with PEG parser
Project description
expression.py
Mathematical expression evaluation library for Python, built with a PEG parser generated by
pegen. An alternative to
safeeval that supports more Ruby operators like =~ and array
operators.
This is a Python port of jcubic/expression.php.
Installation
pip install expression-py
Usage
from expression import Expression
expr = Expression()
# Basic arithmetic
expr.evaluate("2 + 3 * 4") # 14
expr.evaluate("(2 + 3) * 4") # 20
expr.evaluate("2 ** 10") # 1024
expr.evaluate("10 % 3") # 1
# Floating point
expr.evaluate("0.1 + 0.2") # 0.30000000000000004
expr.evaluate("1.5e2") # 150.0
# Hex and binary literals
expr.evaluate("0xFF") # 255
expr.evaluate("0b1010") # 10
# Bitshift operators
expr.evaluate("100 >> 2") # 25
expr.evaluate("100 << 2") # 400
Variables
expr = Expression()
# Assignment
expr.evaluate("x = 10")
expr.evaluate("x + 2") # 12
# Pre-set variables
expr.variables = {"width": 100, "height": 50}
expr.evaluate("width * height") # 5000
Built-in Constants and Functions
expr = Expression()
# Constants
expr.evaluate("pi") # 3.141592653589793
expr.evaluate("e") # 2.718281828459045
# Math functions
expr.evaluate("sqrt(16)") # 4.0
expr.evaluate("sin(pi / 2)") # 1.0
expr.evaluate("abs(-42)") # 42
expr.evaluate("log(e)") # 1.0
Available built-in functions: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh, sqrt, abs, log, ln.
Aliases: arcsin/arccos/arctan/arcsinh/arccosh/arctanh map to their a-prefixed equivalents. ln is an alias for log.
Custom Functions
expr = Expression()
# Define functions in the expression language
expr.evaluate("f(x, y) = x + y")
expr.evaluate("f(10, 20)") # 30
expr.evaluate("square(x) = x * x")
expr.evaluate("square(5)") # 25
# Register Python functions
expr.functions["even"] = lambda x: x % 2 == 0
expr.evaluate("even(4)") # True
# Access defined functions from Python
expr.evaluate("add(a, b) = a + b")
expr.functions["add"](3, 4) # 7
Implicit Multiplication
expr = Expression()
expr.evaluate("f(x) = 2x")
expr.evaluate("f(5)") # 10
expr.evaluate("x = 3")
expr.evaluate("2x") # 6
expr.evaluate("2(3 + 4)") # 14
Boolean and Comparison Operators
expr = Expression()
# Comparisons
expr.evaluate("10 == 10") # True
expr.evaluate("10 != 20") # True
expr.evaluate("10 > 5") # True
expr.evaluate("10 <= 10") # True
# Strict equality (type-aware)
expr.evaluate("2 === 2") # True
expr.evaluate("'2' !== 2") # True
# Boolean operators
expr.evaluate("10 > 5 && 20 > 10") # True
expr.evaluate("10 > 50 || 20 > 10") # True
expr.evaluate("!0") # True
# Keywords
expr.evaluate("true == true") # True
expr.evaluate("null == null") # True
Strings
expr = Expression()
# String literals (single or double quotes)
expr.evaluate('"hello" + " " + "world"') # "hello world"
expr.evaluate("'foo' == 'foo'") # True
# Ruby-style string operators
expr.evaluate('"ab" * 3') # "ababab" (repeat)
expr.evaluate('"a" << "b"') # "ab" (append; mutates a variable)
expr.evaluate('"a" <=> "b"') # -1 (lexicographic compare)
Regular Expressions
expr = Expression()
# Match operator
expr.evaluate('"foobar" =~ /([fo]+)/i') # True
expr.evaluate("$1") # "Foo" (capture group)
expr.evaluate('"hello" =~ /([0-9]+)/') # False
JSON Literals
expr = Expression()
# Objects and arrays
expr.evaluate('{"name": "Alice"}') # {"name": "Alice"}
expr.evaluate('[10, 20, 30]') # [10, 20, 30]
# Property access
expr.evaluate('{"foo": "bar"}["foo"]') # "bar"
expr.evaluate('[10, 20][0]') # 10
# Comparison
expr.evaluate('{"a": 1} == {"a": 1}') # True
expr.evaluate('[1, 2] != [3, 4]') # True
Array Operators
Ruby-inspired operators for concise list manipulation. When one operand is an array and the other is a scalar, the scalar is coerced to a single-element array.
expr = Expression()
# Intersection (&) — common elements, deduped, left order preserved
expr.evaluate("[1, 1, 2, 3] & [3, 4]") # [3]
# Union (|) — combined elements, deduped
expr.evaluate("[1, 2] | [2, 3]") # [1, 2, 3]
# Difference (-) — left elements not in right
expr.evaluate("[1, 2, 2, 3] - [2]") # [1, 3]
# Concatenation (+) — keeps duplicates
expr.evaluate("[1, 2] + [2, 3]") # [1, 2, 2, 3]
# Append (<<) — mutates the left operand when it is a variable
expr.evaluate("[1, 2] << 3") # [1, 2, 3]
# Multiplication (*) — repeat with an integer
expr.evaluate("[1, 2] * 3") # [1, 2, 1, 2, 1, 2]
# Join (*) — with a string separator
expr.evaluate('["a", "b"] * "-"') # "a-b"
# Deep equality (==) and spaceship (<=>)
expr.evaluate("[1, 2] == [1, 2]") # True
expr.evaluate("[1, 2] <=> [1, 3]") # -1
# Membership (in) — array element, or substring of a string
expr.evaluate("2 in [1, 2, 3]") # True
expr.evaluate('"py" in "python"') # True
# Scalar coercion
expr.evaluate("[1, 2, 3] & 2") # [2]
expr.evaluate("1 + [2, 3]") # [1, 2, 3]
When neither operand is an array, these operators fall back to scalar
semantics: & and | are bitwise AND/OR, <</>> are bitshifts, <=>
compares numbers or strings, and +/-/* are arithmetic.
expr.evaluate("6 & 3") # 2 (bitwise AND)
expr.evaluate("6 | 1") # 7 (bitwise OR)
expr.evaluate("5 <=> 3") # 1
Empty arrays are falsy (unlike JavaScript), so they work directly in boolean contexts:
expr.evaluate("!![]") # False
expr.evaluate('[] || "default"') # "default"
expr.evaluate("[] ? 'yes' : 'no'") # "no"
# Validator pattern: true only when at least one skill matches
expr.variables = {"skills": ["Python", "AI"]}
bool(expr.evaluate('skills & ["AI", "ML"]')) # True
Conditional (Ternary) Operator
expr = Expression()
expr.evaluate("1 > 0 ? 'yes' : 'no'") # "yes"
expr.evaluate("[] ? 'yes' : 'no'") # "no"
Error Handling
expr = Expression()
# Errors raise exceptions by default
try:
expr.evaluate("10 / 0")
except ZeroDivisionError:
print("Division by zero!")
# Suppress errors (returns None on error)
expr.suppress_errors = True
expr.evaluate("unknown_var") # None
print(expr.last_error) # error message
Semicolons
Trailing semicolons are optional and ignored:
expr.evaluate("10 + 10;") # 20
Operator Precedence (lowest to highest)
| Precedence | Operators |
|---|---|
| 0 | ? : (ternary) |
| 1 | || |
| 2 | && |
| 3 | ==, !=, ===, !==, =~, <=>, >, <, >=, <=, in |
| 4 | <<, >> |
| 5 | +, -, | |
| 6 | *, /, %, &, [], implicit multiplication |
| 7 | !, unary -, unary + |
| 8 | **, ^ |
Operators & (intersection), \| (union), - (difference), +
(concatenation), << (append), * (repeat/join), == (deep equality), <=>
(spaceship), and in (membership) are type-dispatched: they apply array
semantics when either operand is an array, otherwise scalar semantics.
Development
# Install in development mode
pip install -e ".[dev]"
# Run tests
pytest
# Regenerate parser from grammar
python -m pegen expression/grammar.peg -o expression/parser.py
License
Copyright (c) 2026 Jakub T. Jankiewicz
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file expression_py-0.1.0.tar.gz.
File metadata
- Download URL: expression_py-0.1.0.tar.gz
- Upload date:
- Size: 32.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a6b8e0af8dea2a331aea1ad31d658760d1bc1d42ed4ad5e799815fb2ad9236d
|
|
| MD5 |
1f448243da2f844c4076d76e97092a66
|
|
| BLAKE2b-256 |
f69df42aeb5f60fe5cfe4fc1afca4487399325b8c8199e9e28f002945a00463e
|
Provenance
The following attestation bundles were made for expression_py-0.1.0.tar.gz:
Publisher:
publish.yml on jcubic/expression.py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expression_py-0.1.0.tar.gz -
Subject digest:
2a6b8e0af8dea2a331aea1ad31d658760d1bc1d42ed4ad5e799815fb2ad9236d - Sigstore transparency entry: 1791616789
- Sigstore integration time:
-
Permalink:
jcubic/expression.py@48d223acb8790179d7f83f902035b8e5726d31ed -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/jcubic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48d223acb8790179d7f83f902035b8e5726d31ed -
Trigger Event:
release
-
Statement type:
File details
Details for the file expression_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: expression_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e6c2c3ae70242b9b28c3cadde888707a3fd65c797bc823ad6639653679c9d2e
|
|
| MD5 |
0a0e30b8a5a8c376812443a789db4464
|
|
| BLAKE2b-256 |
ddeea95560f9048a5effdc6566c7b8e46c4048f9b4487c3030dad0e8523fff1e
|
Provenance
The following attestation bundles were made for expression_py-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on jcubic/expression.py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expression_py-0.1.0-py3-none-any.whl -
Subject digest:
5e6c2c3ae70242b9b28c3cadde888707a3fd65c797bc823ad6639653679c9d2e - Sigstore transparency entry: 1791616850
- Sigstore integration time:
-
Permalink:
jcubic/expression.py@48d223acb8790179d7f83f902035b8e5726d31ed -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/jcubic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48d223acb8790179d7f83f902035b8e5726d31ed -
Trigger Event:
release
-
Statement type: