Skip to content

Formulas & Safe Eval

Some values in Fullfinity are not written by a developer — they are authored as data: a statutory tax-return box, a configurable financial-report line, a payroll salary component. Each carries a formula string the user (or a seed file) typed in. Evaluating those strings with Python’s built-in eval would let a formula execute arbitrary code. safe_eval is the blessed way to evaluate them: it parses the expression into an AST and walks it against a strict whitelist, so a malformed or hostile formula raises FormulaError and never runs anything.

Never hand-roll eval/exec for a user- or data-authored formula. Always use safe_eval. It lives in fullfinity/engine/safe_eval.py and safe_eval/FormulaError are re-exported from engine/base.py, so they are available in every module’s from fullfinity.engine.base import *.

def safe_eval(expr: str, names: dict | None = None, functions: dict | None = None):
"""Evaluate `expr` over `names`. Returns a number. Raises `FormulaError`."""
  • expr — the formula string, e.g. "(revenue - cogs) * 0.7".
  • names — maps each bare identifier in the expression to a numeric value ({"revenue": 1000.0, "cogs": 400.0}). A name used in the formula but absent from this mapping raises FormulaError.
  • functions — overrides the default helper allow-list. When omitted, the defaults (abs, min, max, round) apply. Pass your own dict to add domain helpers (see the payroll bracket-table example below) — note that passing a dict replaces the defaults rather than extending them, so include _DEFAULT_FUNCTIONS if you still want them.
from fullfinity.engine.base import safe_eval, FormulaError
result = safe_eval("(revenue - cogs) * margin", {
"revenue": 1000.0,
"cogs": 400.0,
"margin": 0.5,
})
# result == 300.0

FormulaError subclasses ValidationError, so it surfaces to the user as a toast (no traceback) and is caught by except FullfinityError.

The evaluator walks the parsed AST and accepts only:

  • Numeric literalsint and float. bool is rejected explicitly so True/False cannot masquerade as 1/0.
  • Names bound in the supplied names mapping (referenced bare in the expression).
  • Binary operators + - * /, unary + -, and parentheses.
  • Calls to a name in the allow-list (abs, min, max, round by default).

Everything else raises FormulaError: attribute access, subscripts, comprehensions, boolean/comparison operators, calls to any name not in the allow-list, and keyword arguments in a call. A syntax error in the string also raises FormulaError (with the offending expression and the parser’s message).

safe_eval("__import__('os').system('rm -rf /')") # FormulaError — calls a disallowed name
safe_eval("revenue.balance", {"revenue": 1.0}) # FormulaError — attribute access blocked
safe_eval("a if b else c", {...}) # FormulaError — disallowed element
safe_eval("True * 5") # FormulaError — bool literal rejected
safe_eval("round(x, ndigits=2)", {"x": 1.5}) # FormulaError — keyword args blocked

Division by zero does not raise — a / 0 evaluates to 0.0. The design intent is that a blank report line or tax box reads as zero rather than crashing the whole report when one denominator happens to be empty.

safe_eval("net / base", {"net": 100.0, "base": 0.0}) # -> 0.0

When formula lines reference each other (line C = A + B), you must evaluate the dependencies first. extract_names returns the set of bare names a formula references, without evaluating it — exactly what a report engine needs to topologically order lines and to validate that every referenced code exists.

def extract_names(expr: str) -> set:
"""Return the set of bare names referenced in `expr` (for dependency ordering)."""

extract_names is not re-exported from engine/base.py — import it directly:

from fullfinity.engine.safe_eval import safe_eval, extract_names, FormulaError

The configurable financial-report engine uses it to resolve each formula line’s references before evaluating, raising a clear error for an unknown referenced code:

# enterprise/accounting — evaluating a Formula report line
ns = {}
for nm in extract_names(line.formula):
ref = by_code.get(nm)
if ref is None:
raise FormulaError(f"Formula on '{line.label}' references unknown line code '{nm}'")
ns[nm] = await amount_of(ref) # compute each dependency (recursively)
value = float(safe_eval(line.formula, ns))

extract_names is also useful at save time to validate a formula before it is ever run — parse the names, then dry-run the formula with every name bound to 0.0:

from fullfinity.engine.safe_eval import safe_eval, extract_names, FormulaError
try:
names = extract_names(formula or "")
safe_eval(formula, {n: 0.0 for n in names})
except FormulaError as e:
raise UserError(f"Invalid formula: {e}")

Pass a functions dict to extend the callable allow-list — for example a payroll engine that exposes each tax-bracket table as a callable by its code. Because passing functions replaces the defaults, start from _DEFAULT_FUNCTIONS:

from fullfinity.engine.safe_eval import safe_eval, FormulaError, _DEFAULT_FUNCTIONS
funcs = dict(_DEFAULT_FUNCTIONS)
funcs[bracket_table.code] = bracket_table.as_callable() # e.g. income_tax(base)
try:
amount = float(safe_eval(component.formula, dict(ctx), funcs))
except FormulaError as e:
raise UserError(f"Salary component '{component.name}' has an invalid formula: {e}")

Here ctx is the names mapping — the payroll variables in scope (wage, base, prior component codes, …) — and the formula may call only the helpers in funcs.

  • Configurable financial reports (enterprise/accounting) — a Formula report line computes from other lines’ codes.
  • Payroll salary components (hr_payroll) — a Code-type component’s formula computes the line amount from payroll variables and bracket-table helpers.
  • Statutory tax-return boxes — a box’s formula aggregates other box values.

Any new “the user types a formula” feature should reuse safe_eval rather than introducing a second evaluator.