simpleeval safe user formulas with custom names, functions and limits: working versions (tested 2026-10-16)
Evaluate untrusted user formulas with simpleeval using custom names and functions, per-type attribute allowlists, and the built-in power, shift and string size limits.
passed6 passed · 0 failed2026-10-16 21:57 UTC- Status
- passed
- Last passed
- 2026-10-16 21:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
simpleeval@1.0.8pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("simpleeval-safe-expression") (install), or see them on GitHub.
requirements.txt
simpleeval==1.0.8 pytest==9.1.1
formulas.py
from simpleeval import DEFAULT_FUNCTIONS, EvalWithCompoundTypes
SAFE_ATTRS = {
str: {"lower", "upper", "strip", "title"},
int: {"bit_length"},
float: {"is_integer"},
list: {"count", "index"},
}
def evaluate(formula, values=None, functions=None, allowed_attrs=SAFE_ATTRS):
fns = dict(DEFAULT_FUNCTIONS)
fns["clamp"] = lambda x, lo, hi: max(lo, min(hi, x))
fns["total"] = lambda items: sum(items)
if functions:
fns.update(functions)
evaluator = EvalWithCompoundTypes(
names=dict(values or {}),
functions=fns,
allowed_attrs=allowed_attrs,
)
return evaluator.eval(formula)
tests/test_formulas.py
import pytest
from simpleeval import FeatureNotAvailable, FunctionNotDefined, IterableTooLong, NumberTooHigh, simple_eval
from formulas import evaluate
def test_custom_names_functions_and_builtins_kept():
assert evaluate("clamp(qty * price, 0, 100)", {"qty": 5, "price": 30}) == 100
assert evaluate("int('3') + total([1, 2])", {}) == 6
def test_function_registration_gotchas():
with pytest.raises(FunctionNotDefined):
evaluate("double(2)", {"double": lambda x: x * 2})
with pytest.raises(FeatureNotAvailable):
evaluate("1", {}, functions={"danger": eval})
def test_allowed_attrs_is_per_type():
assert evaluate("code.upper()", {"code": "ab"}) == "AB"
assert evaluate("n.bit_length()", {"n": 7}) == 3
with pytest.raises(FeatureNotAvailable):
evaluate("n.real", {"n": 3})
def test_dunder_attrs_blocked_even_without_allowlist():
with pytest.raises(FeatureNotAvailable):
evaluate("x.__class__", {"x": "hi"})
with pytest.raises(FeatureNotAvailable):
evaluate("x.__class__", {"x": "hi"}, allowed_attrs=None)
def test_arithmetic_and_size_limits():
with pytest.raises(NumberTooHigh):
evaluate("9 ** 9 ** 9", {})
with pytest.raises(IterableTooLong):
evaluate("'x' * 10 ** 7", {})
def test_compound_types_need_the_compound_evaluator():
assert evaluate("[n * 2 for n in nums]", {"nums": [1, 2, 3]}) == [2, 4, 6]
assert evaluate("{n: n * n for n in nums}", {"nums": [1, 2]}) == {1: 1, 2: 4}
with pytest.raises(FeatureNotAvailable):
simple_eval("[1, 2]")
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.