A Python library for symbolic math expressions and evaluation.
Project description
Expressionizer
Expressionizer is a Python library for symbolic math expression building, simplification, and step-by-step evaluation output.
It is designed for apps and tools that need explainable algebraic transformation, not just final answers. Typical use cases include math tutoring workflows, educational software, expression debugging, and generating human-readable solution traces.
Pre-Stable Status
Expressionizer is currently in a pre-stable (0.x) phase.
- The core API is usable and actively developed.
- Some interfaces and behavior may change between minor versions.
- If you are using this in production, pin an exact version.
Why Expressionizer
- Build symbolic expressions in Python.
- Evaluate with variable substitutions.
- Generate step-by-step simplification traces.
- Render expressions as plain text and LaTeX.
- Support structured expression types like equations and inequalities.
- Include procedural expression generation utilities.
Features
- Symbolic expression tree primitives
Symbol,Power,Product,Sum,FunctionCallEquationandInEqualitydata structures
- Convenience constructors
symbol(...),sum(...),product(...),power(...),fraction(...)
- Expression normalization and simplification
- Combines numeric terms/factors
- Merges powers and repeated structures where possible
- Step-by-step evaluation engine
evaluate(...)returns both the result and evaluation context- Context tracks snapshots and can render explanation output
- Includes decomposition-based arithmetic steps for larger operations
- Configurable evaluator behavior
- Limits and precision controls via
EvaluatorOptions - Approximation and bounds behavior for very small/large numbers
- Limits and precision controls via
- Rendering
- Plain text rendering with
render(...) - LaTeX rendering with
render_latex(...) - Expression tree inspection with
render_type(...)
- Plain text rendering with
- Function evaluation support
- Works with substitutions for variables and callables
- Includes common math function support through procedural helpers
- Procedural generation utilities
- Random variable name generation
- Random number generation with constraints
- Weighted random expression generation for testing/content generation
- Optional calculus generation controls (
allow_calculus,complexity,guarantee_solvable)
Installation
pip install expressionizer
Quick Start
from expressionizer import symbol, sum, power, evaluate, render_latex
x = symbol("x")
expr = power(sum([x, 2]), 2)
result, context = evaluate(expr, substitutions={"x": 3})
print("Result:", result)
print("Expression (LaTeX):", render_latex(expr))
print(context.render())
Output from a real run:
Result: 25
Expression (LaTeX): (2 + x)^2
## Step 1
Substitute $x = 3$:
$$(2 + x)^2 \\
= (2 + 3)^2 \\
= 5^2$$
## Step 2
$$5^2 \\
= 5(5) \\
= 25$$
Real Examples (Generated by Expressionizer)
1) Symbolic multiplication with substitution
from expressionizer import symbol, sum, product, evaluate
x = symbol("x")
expr = product([sum([x, 4]), sum([x, 1])])
result, context = evaluate(expr, substitutions={"x": 5})
print(result)
print(context.render())
54
## Step 1
Substitute $x = 5$:
$$(4 + x)(1 + x) \\
= (4 + 5)(1 + 5) \\
= 9(1 + 5)$$
## Step 2
$$9(1 + 5) \\
= 9(6)$$
## Step 3
$$9(6) \\
= 54$$
2) Decimal decomposition and place-value addition
from expressionizer import Sum, evaluate
expr = Sum([4, 7.90623])
result, context = evaluate(expr)
print(result)
print(context.render())
11.90623
Let's break $4$ and $7.90623$ down into their components.
$$4 + 7.90623 \\
= 4 + 7 + 0.9 + 0.006 + 0.0002 + 0.00003$$
[aligned place-value rows]
4.00000
7.00000
0.90000
0.00600
0.00020
0.00003
$10^{-5}$: $3 + 0 + 0 + 0 + 0 + 0 = 3$
$10^{-4}$: $0 + 2 + 0 + 0 + 0 + 0 = 2$
$10^{-3}$: $0 + 0 + 6 + 0 + 0 + 0 = 6$
$10^{-1}$: $0 + 0 + 0 + 9 + 0 + 0 = 9$
$10^{0}$: $0 + 0 + 0 + 0 + 7 + 4 = 11$, carry the 1.
$10^{1}$: 1 (carried)
Putting it together, we get $11.90623$.
$$ 4 + 7 + 0.9 + 0.006 + 0.0002 + 0.00003 = 11.90623 $$
context.render() returns a formatted explanation sequence you can display in apps, notebooks, or web UIs.
Core API Overview
evaluate(expression, substitutions={}, error_on_invalid_snap=True, options=None)- Returns
(result, context)
- Returns
compact_evaluator_options(...)- Returns a compact preset for shorter explanations and lower token cost
render(expression, group=False)- Plain text expression rendering
render_latex(expression, renderOptions=...)- LaTeX rendering for display and documentation
- Constructors:
symbol(name)sum(terms)product(factors)power(base, exponent)fraction(numerator, denominator)derivative(expression, variable, order=1)partial_derivative(expression, variables)integral(expression, variable, lower=None, upper=None)
Calculus Coverage Notes
Expressionizer includes a native rule-based calculus engine for derivatives and integrals, including multivariate differentiation and definite/indefinite integrals.
- Coverage is strong for common educational forms (polynomials, many trig/exp/log forms, product/chain/power rules).
- Some advanced integrals and non-elementary forms will remain symbolic (by design) rather than returning incorrect simplifications.
- For procedural generation, prefer
guarantee_solvable=Truewhen you need high reliability for auto-generated calculus problems. - The evaluator now exposes solve metadata (
solve_status,reason_code, coverage tags, explanation events) so you can filter low-confidence outputs in training pipelines.
Explanation Customization
You can tune both evaluator behavior and wording style without changing defaults:
from expressionizer import EvaluatorOptions, WordingOptions, evaluate
result, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
wording_style="concise",
wording_options=WordingOptions(step_heading_template="### Phase {number}"),
),
)
For full per-call wording customization (including per-generation language/profile swaps),
use ExplanationProfile:
from expressionizer import EvaluatorOptions, ExplanationProfile, evaluate
result, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(
locale="en",
missing_key_policy="fallback",
message_overrides={
"step.heading": "## Phase {number}",
"equation.unsupported_equation_arity": "Unsupported equation format.",
},
)
),
)
ExplanationProfile fields:
locale: logical locale tag (used for per-call routing and future locale packs)style_type: built-in style overlay (default|compact|plain|xml)missing_key_policy:fallback|marker|errorfor unresolved localization keysmessage_overrides: key-based template overrides (key -> string)exact_text_overrides: exact-string overrides for legacy/unkeyed text (text -> string)collect_diagnostics: collect localization key-usage/fallback diagnostics during rendering
You can build profiles from named presets:
from expressionizer import build_explanation_profile
profile = build_explanation_profile(
"xml-research",
locale="es", # optional override
collect_diagnostics=True,
)
Preset names:
defaultcompact-researchplain-researchxml-researchspanish-defaultkorean-defaulthebrew-defaulthebrew-niqqud-default
Defaults are production-safe:
- if you do nothing, built-in evaluator/equation templates render normally
- if you override only a few keys, only those keys change
- set
missing_key_policy="error"to require every encountered key be supplied inmessage_overrides - set
missing_key_policy="fallback"(default) to use built-in safe defaults when a key is not overridden - set
missing_key_policy="marker"to surface unresolved keys as[[key]]while debugging
Built-in locale packs currently include:
enesfrdekohehe-niqqud
Changing language per generation:
from expressionizer import EvaluatorOptions, ExplanationProfile, evaluate
# One call in Spanish
_, es_context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(locale="es")
),
)
# Another call in Korean (same expression, different language)
_, ko_context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(locale="ko")
),
)
For dataset generation, pick locale per sample:
import random
from expressionizer import EvaluatorOptions, ExplanationProfile, evaluate, supported_locales
candidate_locales = [loc for loc in supported_locales() if loc != "en"]
def render_one(expr, substitutions):
locale = random.choice(candidate_locales)
_, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(locale=locale)
),
)
return locale, context.render()
Language selection works the same way for equation solving:
from expressionizer import EquationWordingOptions, ExplanationProfile, solve_equation
solution, eq_context = solve_equation(
equation,
wording_options=EquationWordingOptions(
explanation_profile=ExplanationProfile(locale="he-niqqud")
),
)
Built-in locale packs are validated for:
- placeholder safety (no missing/extra formatting placeholders),
- no accidental English leakage for built-in keys in non-English packs.
You can customize formatting structure too (not just wording), including heading style, step wrappers, separators, and line breaks:
from expressionizer import EvaluatorOptions, ExplanationProfile, evaluate
result, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(
message_overrides={
"step.heading": "Step {number}",
"render.newline": " | ",
"render.step.block": "[{heading}] {body}",
"render.step.joiner": " || ",
}
)
),
)
Template layering is supported with {{other.key}} references:
{
"render.step.open": "<s{number}>",
"render.step.close": "</s{number}>",
"render.step.block": "{{render.step.open}}{newline}{body}{newline}{{render.step.close}}"
}
Built-in style overlays can be selected per call:
from expressionizer import EvaluatorOptions, ExplanationProfile, evaluate
_, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
explanation_profile=ExplanationProfile(
locale="es",
style_type="xml",
)
),
)
Structured explanation output is also available for downstream pipelines:
from expressionizer import evaluate, EvaluatorOptions
_, context = evaluate(expr, substitutions=substitutions, options=EvaluatorOptions())
doc = context.render_document() # dict with solve metadata + normalized steps
payload = context.render_json() # JSON string form
Equation contexts support the same API:
solution, solve_context = solve_equation(equation)
eq_doc = solve_context.render_document()
eq_json = solve_context.render_json()
Language-pack quality tooling:
python -m expressionizer.localization_catalog --output localization_keys.json- Generates a canonical key catalog for translators and reviewers.
python -m expressionizer.localization_catalog --validate-only- Validates built-in locale packs against the full catalog, including runtime fallback gaps.
python -m expressionizer.localization_catalog --validate-only --print-coverage- Prints per-locale key coverage summary using the catalog.
python -m expressionizer.localization_catalog --validate-only --require-full-locale-coverage- Fails validation if any non-English locale still falls back to English for tracked keys.
For long numeric arithmetic, you can enable/configure calculator mode. This is useful for student-friendly readability and for AI training-data workflows that need tool-call placeholders:
from expressionizer import CalculatorModeOptions, EvaluatorOptions, evaluate
result, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(
calculator_mode=CalculatorModeOptions(
enabled=True,
multiplication_operand_complexity_threshold=6,
result_complexity_threshold=20,
template="<tool_call name='calculator' op='{operation}' expr='{expression}' />\nResult: {result}",
)
),
)
Available calculator placeholders include:
{operation}:addition,subtraction,multiplication, orpower{expression}: rendered expression{result}: rendered final value{lhs},{rhs}: left/right operands for binary operations
Default behavior now includes calculator mode with practical thresholds so large numeric arithmetic does not produce excessively long place-value traces.
Default behavior also enables order-of-magnitude estimation for very large numeric operations to prevent runaway explanation size. You can tune or disable it per call:
from expressionizer import EvaluatorOptions, evaluate
# Disable order-of-magnitude estimation (force exact behavior where possible)
result, context = evaluate(
expr,
substitutions=substitutions,
options=EvaluatorOptions(order_of_magnitude_threshold=None),
)
Choosing Profiles
Expressionizer now supports two practical generation profiles:
realistic(default): intended for user-facing outputs and classroom-style workflows- tighter expression complexity bounds
- more predictable explanation length
stress: intended for robustness/fuzz testing- broader expression shapes and higher complexity ceilings
Use realistic for production user experiences, and stress for QA pipelines.
You can also control whether generated expression problems are solvable:
solvability_mode="solvable": intentionally avoids injected impossible/domain-invalid casessolvability_mode="mixed"(default): mostly solvable, with occasional intentionally unsolvable problemssolvability_mode="unsolvable": intentionally generates impossible/domain-invalid problems for negative examples
For mixed, use unsolvable_probability to tune the share of impossible cases, and
hard_problem_probability to occasionally promote a realistic case into a harder-but-still-bounded one.
API Stability Note
For 0.8.x, these configuration surfaces are intended to remain stable:
EvaluatorOptionsWordingOptionsCalculatorModeOptionsgenerate_random_expression(..., generation_profile=...)generate_random_expression(..., complexity=...)generate_random_expression(..., solvability_mode=..., unsolvable_probability=..., hard_problem_probability=...)ExplanationProfile(per-call localization/customization layer)- CLI flags:
--generation-profile,--solvability-mode,--unsolvable-probability,--hard-problem-probability,--wording-style,--compact-explanations,--step-heading-template,--locale,--messages-file,--exact-text-overrides-file
For compact output presets:
from expressionizer import compact_evaluator_options, evaluate
result, context = evaluate(
expr,
substitutions=substitutions,
options=compact_evaluator_options(step_heading_template="### Step {number}"),
)
CLI tools also support:
--wording-style verbose|concise--compact-explanations--step-heading-template "### Phase {number}"--locale en--style-type default|compact|plain|xml--profile-preset <name>--collect-localization-diagnostics--messages-file path/to/messages.json--exact-text-overrides-file path/to/exact_text_overrides.json--generation-profile realistic|stressrealisticis user-facing and avoids extreme expression blowupsstressis broader/extreme for robustness testing
--solvability-mode mixed|solvable|unsolvablemixedis best for real-world distributions with some impossible examplessolvableis best for student practice datasets where every case should resolveunsolvableis best for negative datasets teaching solvability detection
--unsolvable-probability 0.12- used when
--solvability-mode mixed
- used when
--hard-problem-probability 0.2- occasionally escalates a realistic problem into a harder variant
--complexity-cycle 0.2,0.5,0.8- profile sweep for continuous generation complexity in stress/audit tools
--problem-families sum,product,power,function,derivative,integral- constrain expression generation to selected structural families
--equation-families linear,quadratic,rational,system- constrain equation generation to selected equation families
Quality and Audit Utilities
python -m expressionizer.procedural_test ...- Stress-tests generated expressions/equations until a failure or timeout
python -m expressionizer.explanation_audit ...- Audits explanation consistency and optional SymPy equivalence
python -m expressionizer.manual_review_cases --cases 40 --generation-profile realistic- Generates user-facing manual-review samples with safer default complexity
python -m expressionizer.manual_review_cases --cases 40 --solvability-mode mixed --unsolvable-probability 0.15- Generates mixed datasets with both solvable and intentionally impossible cases
python -m expressionizer.manual_review_cases --cases 40 --problem-families derivative,integral- Generates only selected expression families for focused review
python -m expressionizer.equation_manual_review_cases --cases 40- Generates a markdown set of equation/system explanations for manual review
- Supports localization overrides with
--locale,--messages-file, and--exact-text-overrides-file
python -m expressionizer.equation_manual_review_cases --cases 40 --equation-families quadratic- Constrains generated equation cases to selected equation families
python -m expressionizer.localization_catalog --validate-only- Validates locale packs against catalog coverage + placeholder consistency
python release_smoke.py- Runs a quick pre-release local smoke check
Compatibility
- Python
>=3.8 - OS independent
Roadmap
As a pre-stable library, near-term improvements are focused on:
- API stabilization toward
1.0 - Expanded test coverage
- Improved docs and examples
- Continued refinement of step-by-step output quality
SEO Keywords
Python symbolic math library, step-by-step math solver, algebra expression evaluator, LaTeX math renderer, expression simplification engine, educational math software backend.
Contributing
Issues and pull requests are welcome. If you report a bug, include:
- the expression
- substitutions used
- expected behavior
- actual behavior and rendered steps
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file expressionizer-0.8.3.tar.gz.
File metadata
- Download URL: expressionizer-0.8.3.tar.gz
- Upload date:
- Size: 100.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b18f9b117ebbe16c40e7cf3ba22199cb22f4dc43436d5bc0458d84e4f779eb89
|
|
| MD5 |
25fe97a97b9a8a72d05e34ea11e5cc3e
|
|
| BLAKE2b-256 |
30cc401cf3129a481166b74a48c40021abdff1664f1f110ccbbefa9b5b2b9413
|
File details
Details for the file expressionizer-0.8.3-py3-none-any.whl.
File metadata
- Download URL: expressionizer-0.8.3-py3-none-any.whl
- Upload date:
- Size: 100.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d28b93e08a8141ca5a0d00f8dd262bc449b02ab237f578736943f287ffc65c6
|
|
| MD5 |
4eb1e725bcf23590e9be49373a7df746
|
|
| BLAKE2b-256 |
c03444cf91f0b847cc796e548f3f7f78eeb13ae6edf4503b751b95288cd7a807
|