Skip to main content

Mathematical expression evaluation library with PEG parser

Project description

expression.py

pip CI PyPI Downloads expression.py GitHub repo Coverage Status LICENSE GPL-3.0

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

expression_py-0.1.0.tar.gz (32.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

expression_py-0.1.0-py3-none-any.whl (29.0 kB view details)

Uploaded Python 3

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

Hashes for expression_py-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2a6b8e0af8dea2a331aea1ad31d658760d1bc1d42ed4ad5e799815fb2ad9236d
MD5 1f448243da2f844c4076d76e97092a66
BLAKE2b-256 f69df42aeb5f60fe5cfe4fc1afca4487399325b8c8199e9e28f002945a00463e

See more details on using hashes here.

Provenance

The following attestation bundles were made for expression_py-0.1.0.tar.gz:

Publisher: publish.yml on jcubic/expression.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for expression_py-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5e6c2c3ae70242b9b28c3cadde888707a3fd65c797bc823ad6639653679c9d2e
MD5 0a0e30b8a5a8c376812443a789db4464
BLAKE2b-256 ddeea95560f9048a5effdc6566c7b8e46c4048f9b4487c3030dad0e8523fff1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for expression_py-0.1.0-py3-none-any.whl:

Publisher: publish.yml on jcubic/expression.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page