A small arithmetic expression evaluator in pure Python (standard library only). It is a hand-written mini parser/interpreter:
tokenizer -> shunting-yard (to RPN) -> RPN evaluation
It does not use Python's eval or ast internally. Every step is done by
hand. Python's own evaluation is used only in the test suite as a reference
oracle, to prove the hand-written evaluator agrees with Python's semantics.
Library:
from expr_eval import evaluate, ExprError
evaluate("2 + 3 * (4 - 1) ** 2") # -> 29
evaluate("-2**2") # -> -4 (** binds tighter than unary minus)
evaluate("2**3**2") # -> 512 (** is right-associative)Command line:
$ python expr_eval.py "2 + 3 * (4 - 1) ** 2"
29evaluate(expr) returns an int or a float and raises ExprError on any
malformed input or division by zero.
- Integer literals (
12), floats (3.14,.5), scientific notation (1e3,2.5E-4). - Binary operators:
+ - * / // % **. - Parentheses
(). - Unary
+and-(including chains like--3,+-3).
**is right-associative and has higher precedence than unary minus, so-2**2 == -4and2**3**2 == 512.//is floor division:7//2 == 3,-7//2 == -4.%uses Python's sign convention:7 % -3 == -2,-7 % 3 == 2./is true division and always yields afloat.- Normal precedence and left-associativity otherwise
(
2 + 3 * 4 == 14,10 - 2 - 3 == 5).
These semantics are verified by fuzzing 5000 randomly generated valid
expressions against Python's own evaluation (int results must be exactly equal;
float results must satisfy math.isclose(rel_tol=1e-9)), plus targeted
semantics tests.
ExprError is raised for:
- invalid characters (
"@","2 & 3"), - unbalanced parentheses (
"(1+2","1+2)"), - missing operands (
"2 +","2 ** ","*","2 3"), - division by zero (
"1/0","5 % 0","5 // 0") — Python'sZeroDivisionErroris caught at the computation point and re-raised asExprError, - empty input.
- No variables, no named constants, no functions (
sin,pi, ... are not supported) — arithmetic only. - No bitwise operators (
& | ^ ~ << >>) and no comparison/boolean operators. - No
eval/astinternally. The tokenizer, shunting-yard parser, and RPN evaluator are written by hand on purpose. Python is used solely as a test oracle. - Semantics are "Python-flavoured" and tested as such, not an independent specification: the goal is to agree with CPython's arithmetic on the supported subset.
- No arbitrarily large
**. The fuzz generator restricts exponents to0..4with small bases. This is deliberate — the tests check semantics, not big-number performance, and avoid meaningless overflow.evaluateitself imposes no such limit; you can pass large exponents directly, but they are outside what the fuzz test exercises.
Run the tests (hermetic, fixed seed):
$ python -m unittest -vMIT — see LICENSE.