sympy_wolfram — translating Wolfram FullForm into SymPy
⚠️ Experimental — this package is under active development; APIs, rule content and behaviour may change without notice. Version 0.0.2 is a pre-alpha snapshot.
This package turns Mathematica expressions into SymPy ones. Its most delicate job is translating patterns, because Mathematica distinguishes three things that all print as the same letter, and collapsing any two of them silently changes which expressions a rule fires on.
Every example below is a doctest, executed by sympy_wolfram/tests/test_readme.py.
For what the resulting objects then mean when matching, see
sympy_matching/README.md.
1. The entry point
ffl_to_sympy_code takes a Full-Form List (nested Python lists mirroring Mathematica's
FullForm) and returns Python source, the wildcard definitions it needs, and the plain
symbols it saw.
>>> from sympy_wolfram.interpreter import ffl_to_sympy_code
>>> code, wild_defs, symbols = ffl_to_sympy_code(['Power', 'x', '2'], {'x': 'x'})
>>> code
'(x)**(Integer(2))'
The returned code is meant to be eval'd in a namespace the converter fills in for you:
>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Plus', 'x', '1'], {'x': 'x'}, ns)
>>> eval(code, ns)
x + 1
2. The three pattern forms
| Mathematica | FullForm |
translation |
|---|---|---|
d |
d |
Symbol('d') — a literal |
d_ |
Pattern[d, Blank[]] |
d_ — WildSymbol('d') |
d_. |
Optional[Pattern[d, Blank[]]] |
_d_ — WildSymbol('d', optional_value=IDENTITY_ELEMENT) |
Note the naming convention: the plain Blank becomes d_ (trailing underscore) and
the Optional Blank becomes _d_ (leading and trailing). Both carry the OmniMatch
variable name d, which is what unifies them — see sympy_matching/README.md §2.
>>> blank = ['Pattern', 'd', ['Blank']]
>>> optional = ['Optional', ['Pattern', 'd', ['Blank']]]
>>> code, defs, _ = ffl_to_sympy_code(['Plus', blank, ['Times', optional, 'W']])
>>> code
"(d_ + (_d_ * Symbol('W')))"
>>> sorted(defs)
["_d_ = WildSymbol('d', optional_value=IDENTITY_ELEMENT)", "d_ = WildSymbol('d')"]
This is Rubi's d_ + d_.*ProductLog[...] shape: a denominator d*(1 + W) where both
occurrences are the same variable.
3. Inside a pattern, a bare atom is a LITERAL
Mathematica keeps a literal symbol independent of a same-named pattern variable —
MatchQ[d + 5 W, d + d_.*W] is True, because the literal d matches itself while the
variable binds 5. So a bare atom appearing in a pattern must not be rewritten into
the wildcard:
>>> code, _, _ = ffl_to_sympy_code(['Plus', 'd', ['Times', optional, 'W']])
>>> code
"(Symbol('d') + (_d_ * Symbol('W')))"
Plus is orderless, so the very same Mathematica expression can arrive with its
arguments the other way round. The translation must be identical:
>>> code, _, _ = ffl_to_sympy_code(['Plus', ['Times', optional, 'W'], 'd'])
>>> code
"((_d_ * Symbol('W')) + Symbol('d'))"
Both keep Symbol('d') a literal. (This used to be order-dependent: the sets of
discovered wildcards fill up as the converter walks the tree, so a literal appearing
after the wildcard was rewritten into it, producing a pattern that demanded both be
equal and no longer matched d + 5 W.)
4. Pattern side vs replacement side
A rule's replacement and constraints contain no Pattern[...] nodes at all —
their wildcards appear as bare atoms, and there they must resolve to the bound
values. The caller signals this by pre-seeding the names:
>>> code, _, _ = ffl_to_sympy_code(['Times', 'd', 'W'], wildcards={'d'})
>>> code
"(d_ * Symbol('W'))"
>>> code, _, _ = ffl_to_sympy_code(['Times', 'd', 'W'], optional_wildcards={'d'})
>>> code
"(_d_ * Symbol('W'))"
So the rule is:
- no
wildcards=/optional_wildcards=given → this is a pattern; bare atoms stay literalSymbols; - names given → this is a replacement/constraint; bare atoms resolve to wildcards.
5. reserved_symbols — names bound by the caller
A name the caller already binds (classically the integration variable, from
x_Symbol) must never become a wildcard. Pass it in reserved_symbols:
>>> code, _, _ = ffl_to_sympy_code(['Times', 'a', 'x'], {'x': 'x'}, wildcards={'a'})
>>> code
'(a_ * x)'
x is emitted as the bare identifier x, not Symbol('x') and not a wildcard.
There is a subtlety worth knowing: x_. where x is also reserved. Both bind the
same name, so the "absent" branch would have to give x the Times identity 1, which
then fails x_Symbol. The optional branch is therefore unreachable and the factor is in
fact mandatory — verified in Mathematica, where g[x_.*h[x_], x_Symbol] matches
g[z h[z], z] but not g[h[z], z].
6. Constants, numbers and heads
Known constants and functions are emitted fully qualified, so the generated code is unambiguous no matter what the surrounding module has imported:
>>> ffl_to_sympy_code('Pi')[0]
'sympy.pi'
>>> ffl_to_sympy_code(['Sin', 'x'], {'x': 'x'})[0]
'sympy.sin(x)'
>>> ffl_to_sympy_code(['Power', 'x', '2'], {'x': 'x'})[0]
'(x)**(Integer(2))'
Integers become Integer(...) rather than Python ints, so exact arithmetic is
preserved (Integer(1)/Integer(4) is a Rational, whereas 1/4 would be a float):
>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Times', '3', 'x'], {'x': 'x'}, ns)
>>> code
'(Integer(3) * x)'
>>> eval(code, ns)
3*x
7. Round trip: translate, then match
Putting §2–§4 together — translate a Wolfram pattern and use it. The values below are
Mathematica's own answers for MatchQ[..., d + d_.*W]:
>>> from sympy import Symbol
>>> from omnimatch import ManyToOneMatcher, Pattern
>>> from sympy_matching.matching_rule import to_omnimatch_expression
>>> x, W, d = Symbol('x'), Symbol('W'), Symbol('d')
>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Plus', 'd', ['Times', optional, 'W']], namespace=ns)
>>> pattern = eval(code, ns)
>>> def matches(pat, subject):
... m = ManyToOneMatcher()
... m.add(Pattern(to_omnimatch_expression(pat)))
... return bool(list(m.match(to_omnimatch_expression(subject))))
>>> matches(pattern, d + d*W) # Mathematica: True
True
>>> matches(pattern, 5 + 5*W) # Mathematica: False -- literal d is not 5
False
>>> matches(pattern, d + 5*W) # Mathematica: True -- variable binds 5
True
Why this matters
A rule that fires where Mathematica's would not produces a wrong antiderivative, not
an error — the replacement derived for one shape gets applied to another. The three
forms therefore have to stay distinct all the way from FullForm to the OmniMatch
pattern, which is why they are pinned by tests at both layers:
sympy_wolfram/tests/test_blank_optional_semantics.py— 37 cases, every expected value read directly off Mathematica 12.2;sympy_wolfram/tests/test_docs.pyandsympy_matching/tests/test_docs.py— these documents.
See also
docs/translating-mathematica.md— the full pipeline: notation → Full-Form List → SymPy code → live rule, with a worked example.docs/nodes-and-evaluation.md— deferredMathematicaExprnodes,doit()vsrewrite_as_standard_sympy(),eager_<Name>functions, and the constraint classes.sympy_matching/docs/— the matching layer this package builds on.
Testing
pytest tests/ — a handful of tests exercise the re-exports that the
higher-level rubi-integrate package builds on top of this one; they need
rubi-integrate importable (it is not a runtime dependency of this package).
License
MIT License, Copyright (c) 2026 Francesco Bonazzi. See LICENSE.
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 sympy_wolfram-0.0.3.tar.gz.
File metadata
- Download URL: sympy_wolfram-0.0.3.tar.gz
- Upload date:
- Size: 103.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a07f53e27041bd58a2dd68a106e89a3654c72bbd39d120530f52efa6096369a9
|
|
| MD5 |
6fa3bb232ae4ef91b89d3ea3b3481894
|
|
| BLAKE2b-256 |
227d9610c023cbfdabeb0715dbb273a00bd0f6e4436cb983efbd7a759f301902
|
File details
Details for the file sympy_wolfram-0.0.3-py3-none-any.whl.
File metadata
- Download URL: sympy_wolfram-0.0.3-py3-none-any.whl
- Upload date:
- Size: 62.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4702c62ff3a281a6c867567e97774c0a5366c9742c64dbe14efd7f3c7c08352f
|
|
| MD5 |
9bcb3f6e5a1f54a34cd2ea94b0dd7530
|
|
| BLAKE2b-256 |
5e0aafae62c12b2f12afe310cc64c118793e3a1a33b73db28fe8e8bdb8e7516f
|