subs v. replace v. xreplace - help needed on a minimal viable example of their difference

57 views
Skip to first unread message

Conrad Schiff

unread,
Dec 25, 2025, 10:57:25 AM12/25/25
to 'Martin Fuller' via sympy
All,

  I am trying to understand the actual differences between the 'subs', 'replace', and 'xreplace'.  I've reviewed the documentation (some of it seems stale) and various discussions in this group.  Perhaps I've missed something but I've not found a side-by-side minimal viable example of that shows when to pick amongst these.

  Does such a thing exist?  If it doesn't is there any interest in making such a thing.  I would be happy to take the first draft of documenting this if (and only if) I can get help from the rest of the group.

Thank you,
Conrad Schiff, PhD
Professor of Physics
Capitol Technology University

Oscar Benjamin

unread,
Dec 27, 2025, 8:03:50 AM12/27/25
to sy...@googlegroups.com
Hi Conrad,

Maybe the docs for this can be improved. In fact there should probably
be a separate docs page for exactly this that perhaps also covers some
other common methods such as rewrite.

The xreplace function is the simplest. The signature is

expr2 = expr1.xreplace({old1: new1, old2: new2, ...})

This will recursively walk down through the expression tree replacing
exact subexpressions so that if old1 is somewhere in expr1 then expr2
will have new1 in that place instead:

In [9]: expr1 = x**2 + cos(x)

In [10]: print(expr1)
x**2 + cos(x)

In [11]: print(expr1.xreplace({cos(x): sin(x)}))
x**2 + sin(x)

For all of xreplace, replace and subs the expression tree is evaluated
as it is rebuilt so often inserting new1 causes some part of the
expression to transform e.g. here cos(pi/4) evaluates to sqrt(2)/2 and
(pi/4)**2 evaluates to pi**2/16:

In [12]: print(expr1.xreplace({x: pi/4}))
pi**2/16 + sqrt(2)/2

The replace function is like xreplace but gives more flexibility for
selecting which expressions to replace and what to replace them by.
There are several different options for what the arguments to replace
can be but they all allow for more complex matching than xreplace
which only matches exact subexpressions. This version uses Wild
symbols for pattern matching:

In [19]: expr1 = sin(x) + sin(y)

In [20]: print(expr1)
sin(x) + sin(y)

In [21]: a = Wild('a')

In [22]: print(expr1.replace(sin(a), cos(a)))
cos(x) + cos(y)

It is also possible to pass functions as argument to replace so that
the full Turing complete Python language can be used to select
subexpressions and compute their replacements:

In [24]: print(expr1.replace(lambda e: e.func == cos, lambda e:
sin(e.args[0])))
sin(x) + sin(y)

Both xreplace and replace are purely structural operations on
expression trees. They do not attempt to interpret anything in any
mathematical way and will just replace the exact subexpressions as
instructed. The subs method is supposed to correspond to performing a
mathematically correct substitution meaning that it will interpret a
match differently:

In [31]: print(expr1)
x**4 + x**3 + x**2 + x

In [32]: print(expr1.subs(x**2, y))
x**3 + x + y**2 + y

Here subs decided that even powers of x could be replaced even if e.g.
x**4 was not the exact replacement pattern but it decided against
replacing odd powers since it does not know whether x is sqrt(y) or
-sqrt(y).

Usually when subs is used you do not have this and the replacement
patterns are all just symbol to expression like expr.subs(x,
some_expr). In that case the primary distinction between xreplace and
subs is really that subs distinguishes between free and bound symbols
whereas xreplace does not:

In [33]: expr = Integral(x*y, (x, 0, 1))

In [34]: print(expr)
Integral(x*y, (x, 0, 1))

In [35]: print(expr.xreplace({x:z, y:t}))
Integral(t*z, (z, 0, 1))

In [36]: print(expr.subs({x:z, y:t}))
Integral(t*x, (x, 0, 1))

In [37]: expr.free_symbols
Out[37]: {y}

These are part of a more general feature that different types of
expression can override the behaviour of subs in a way that does not
happen for xreplace and replace. The _eval_subs method is used for
this and you can see that there are many of these methods defined in
the codebase for different kinds of expression (the ones above were
for Pow in power.py and for Integral in expr_with_limits.py):

$ git grep 'def _eval_subs'
sympy/algebras/quaternion.py: def _eval_subs(self, old: Expr,
new: Expr) -> Quaternion: # type: ignore
sympy/concrete/expr_with_limits.py: def _eval_subs(self, old, new):
sympy/core/add.py: def _eval_subs(self, old, new):
sympy/core/basic.py: def _eval_subs(self, old: Basic, new: Basic)
-> Basic | None:
sympy/core/function.py: def _eval_subs(self, old, new):
sympy/core/function.py: def _eval_subs(self, old, new):
sympy/core/function.py: def _eval_subs(self, old, new):
sympy/core/mul.py: def _eval_subs(self, old, new):
sympy/core/numbers.py: def _eval_subs(self, old, new):
sympy/core/numbers.py: def _eval_subs(self, old, new):
sympy/core/numbers.py: def _eval_subs(self, old, new):
sympy/core/power.py: def _eval_subs(self, old, new):
sympy/core/symbol.py: def _eval_subs(self, old, new):
sympy/functions/elementary/exponential.py: def _eval_subs(self, old, new):
sympy/functions/elementary/piecewise.py: def _eval_subs(self, old, new):
sympy/functions/elementary/trigonometric.py: def _eval_subs(self,
old, new):
sympy/geometry/curve.py: def _eval_subs(self, old, new):
sympy/geometry/entity.py: def _eval_subs(self, old, new):
sympy/logic/boolalg.py: def _eval_subs(self, old, new):
sympy/logic/boolalg.py: def _eval_subs(self, old, new):
sympy/logic/boolalg.py: def _eval_subs(self, old, new):
sympy/physics/control/lti.py: def _eval_subs(self, old, new):
sympy/physics/units/quantities.py: def _eval_subs(self, old, new):
sympy/polys/polytools.py: def _eval_subs(f, old, new):
sympy/polys/rootoftools.py: def _eval_subs(self, old, new):
sympy/series/formal.py: def _eval_subs(self, old, new):
sympy/series/fourier.py: def _eval_subs(self, old, new):
sympy/series/order.py: def _eval_subs(self, old, new):
sympy/sets/conditionset.py: def _eval_subs(self, old, new):
sympy/tensor/tensor.py: def _eval_subs(self, old, new):

The xreplace function is the fastest and simplest. The replace
function is the most flexible. The subs function is the slowest and is
the only one that applies any semantic meaning to the substitution.

--
Oscar
> --
> You received this message because you are subscribed to the Google Groups "sympy" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to sympy+un...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/sympy/CH3PR12MB94327C143742026A16782408CFB2A%40CH3PR12MB9432.namprd12.prod.outlook.com.

Conrad Schiff

unread,
Dec 27, 2025, 8:07:33 AM12/27/25
to sy...@googlegroups.com
Hi Oscar,

  Thank you for the thorough reply.  I will digest it and try to put a beginner touch on it.  Would you be willing to review it and, if it is acceptable, I would be happy for it to be used in the documentation.  I don't know the development hierarchy so feel free to point me to other people, in other directions, or suggest other things (including to stand down on behalf of SymPy 🙂).

Conrad

From: sy...@googlegroups.com <sy...@googlegroups.com> on behalf of Oscar Benjamin <oscar.j....@gmail.com>
Sent: Saturday, December 27, 2025 8:03 AM
To: sy...@googlegroups.com <sy...@googlegroups.com>
Subject: Re: [sympy] subs v. replace v. xreplace - help needed on a minimal viable example of their difference
 

Oscar Benjamin

unread,
Dec 28, 2025, 2:57:06 PM12/28/25
to sy...@googlegroups.com
Hi Conrad,

Yes, I can review a pull request for this.

Probably the place to put it is in doc/src/explanation/ but it could
also make sense to go in the intro tutorial.

Oscar

On Sat, 27 Dec 2025 at 13:07, 'Conrad Schiff' via sympy
> To view this discussion visit https://groups.google.com/d/msgid/sympy/CH3PR12MB9432C4D8378063949646B961CFB1A%40CH3PR12MB9432.namprd12.prod.outlook.com.
Reply all
Reply to author
Forward
0 new messages