A small Flask service that puts a few SymPy operations behind HTTP endpoints. You POST a math expression as JSON and get the simplified, solved, or numerically evaluated result back as a string. It exists so a program that cannot import SymPy directly — an agent loop, a notebook on a locked-down host, a spreadsheet — can still call out for a symbolic answer.
It is one file, 338 lines. There is no database, no auth, no user model, and no queue. If you need symbolic math inside a Python process, import SymPy instead; this is only useful when the caller is somewhere else.
SymPy's parse_expr tokenizes the string you give it and then calls eval() on the
result. On unvalidated input that is remote code execution, not a theoretical one — before
the checks below existed, posting ().__class__ returned Python's tuple class, which is
the first step of the usual walk to object.__subclasses__().
The service now validates every expression before it reaches parse_expr:
- a 500 character cap
- a character allow-list — no underscore (blocks dunder names), no brackets (blocks
indexing and comprehensions), no colon (blocks
lambda), no quotes - a reject on attribute access (
.followed by a letter;1.5still parses) - an explicit function allow-list — any name used as a call must be one of the 23 SymPy
functions in
SAFE_GLOBALS, andparse_expris given that restrictedglobal_dict, so an unlisted name can only ever become a plainSymbol - variable names must match
^[A-Za-z][A-Za-z0-9]*$before going tosymbols()
That is defence in depth, not a sandbox. It closes the paths that are easy to reach from a
JSON body; it is not a proof that none remain. Do not put this on the public internet
without a real isolation boundary — a container with no network egress, a seccomp
profile, a CPU and memory limit. There is also no authentication and no rate limiting, and
simplify and integrate can be made to run for a very long time on a hostile input.
It is built to run locally, or on a private instance you control. That is the intended use.
All take POST with a JSON body. Errors come back as {"error": "..."} with a 400.
| Endpoint | Body | Returns |
|---|---|---|
GET / |
— | service name and endpoint list |
/algebra/solve |
{"expression": "x^2 - 4", "variable": "x"} |
{"solutions": ["-2", "2"]} |
/algebra/simplify |
{"expression": "(x^2 - 1)/(x - 1)"} |
{"result": "x + 1"} |
/geometry/solid |
{"expression": "V = 4/3*pi*r^3; r = 2"} |
{"result": "33.5103216382911"} |
/combinatorics |
{"expression": "binomial(10, 3)"} |
{"result": "120.000000000000"} |
/number-theory |
{"expression": "gcd(48, 18)"} |
{"result": "6.00000000000000"} |
/olympiad/advanced |
see below | {"result": "2*x + 2"} |
^ means exponentiation, not XOR — convert_xor is enabled because that is what people
write in a math expression.
/geometry/solid takes a formula, then semicolon-separated numeric substitutions. The
left-hand side of the first part (V =) is discarded; only the right-hand side is parsed.
/olympiad/advanced applies up to 20 operations in order, carrying the result forward:
{
"steps": [
{"operation": "expand", "expression": "(x + 1)^2"},
{"operation": "differentiate", "variable": "x"}
]
}Operations: simplify, solve, substitute, expand, factor, differentiate,
integrate.
Example call:
curl -X POST http://localhost:5000/algebra/solve \
-H "Content-Type: application/json" \
-d "{\"expression\": \"x^2 - 4\", \"variable\": \"x\"}"There is no /geometry/plane. It used to exist and was a character-for-character copy of
/algebra/simplify, so it was removed; plane-geometry expressions go through
/algebra/simplify like anything else.
pip install -r requirements.txt
pip install pytest # tests only, not a runtime dependency
python -m pytest test_app.py # 22 tests
python app.py # dev server on 127.0.0.1:5000python app.py is for local work only. It binds to loopback and runs with debug=False —
the Werkzeug debugger exposes an interactive Python console to anyone who can reach the
port, so it stays off.
Deployment goes through gunicorn. render.yaml starts gunicorn app:app rather than
python app.py, which would have executed the __main__ block and started the
development server in production.
- Results are returned as strings from
str(), not structured math. The caller has to parse them back. evalf()output carries SymPy's default 15 digits, sogcd(48, 18)reads as6.00000000000000.- No auth, no rate limiting, no request timeout beyond gunicorn's.
- The endpoint split is mostly cosmetic.
/combinatoricsand/number-theorydiffer only in that the first defers evaluation while parsing; the names document intent for the caller, nothing more.