Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

expr-eval

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.

Usage

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"
29

evaluate(expr) returns an int or a float and raises ExprError on any malformed input or division by zero.

Supported grammar

  • Integer literals (12), floats (3.14, .5), scientific notation (1e3, 2.5E-4).
  • Binary operators: + - * / // % **.
  • Parentheses ( ).
  • Unary + and - (including chains like --3, +-3).

Semantics: matched to Python

  • ** is right-associative and has higher precedence than unary minus, so -2**2 == -4 and 2**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 a float.
  • 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.

Errors

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's ZeroDivisionError is caught at the computation point and re-raised as ExprError,
  • empty input.

What it does NOT do

  • No variables, no named constants, no functions (sin, pi, ... are not supported) — arithmetic only.
  • No bitwise operators (& | ^ ~ << >>) and no comparison/boolean operators.
  • No eval / ast internally. 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 to 0..4 with small bases. This is deliberate — the tests check semantics, not big-number performance, and avoid meaningless overflow. evaluate itself imposes no such limit; you can pass large exponents directly, but they are outside what the fuzz test exercises.

Development

Run the tests (hermetic, fixed seed):

$ python -m unittest -v

License

MIT — see LICENSE.

About

An arithmetic expression evaluator from scratch in pure Python (tokenizer, shunting-yard, RPN - no eval/ast), matching Python's own result exactly over thousands of random expressions.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages