2025, Oct 17 10:00

Substitute Free Variables in SymPy linsolve Results with subs, Tuple, and free_symbols

Learn to concretize SymPy linsolve solutions by substituting free variables with subs on Tuple and free_symbols. Zeroing, integers, exact rationals with S(1)/3

When SymPy’s linsolve returns a parametric solution, it often contains free variables. Concretizing that solution typically means substituting values for those parameters. The challenge is doing it cleanly and consistently, especially when the set of free variables changes across systems.

Problem setup

Suppose you have a solution vector coming from linsolve where several symbols are free. For illustration, consider this structure:

from sympy import symbols
y2, y4, y5, y6, y7, y8 = symbols('y2 y4 y5 y6 y7 y8')
ans_vec = (
    -2.0*y2 - 2.0,
    4.0*y4 + 4.0*y7 + 2.0,
    y2,
    -2.0*y4 - 1.0*y5 - 2.0*y7 - 2.0,
    y4,
    y5,
    y6,
    y7,
    y8,
)

Here y2, y4, y5, y6, y7, y8 are the free variables. If you want a concrete instance, you might substitute them with zero to obtain a numerical vector like (-2.0, 2.0, 0, -2.0, 0, 0, 0, 0, 0). Doing this reliably without manual filtering requires a substitution-friendly container and a stable way to collect the symbols involved.

What’s really going on

SymPy expressions can be substituted using subs, but native Python tuples don’t carry SymPy’s substitution mechanics. The key is to wrap the solution entries into a SymPy-aware container so that subs can traverse the structure. Another practical detail is that the set of free variables varies, so hand-picking names becomes error-prone. Using the expression’s own free_symbols solves that by asking SymPy directly which symbols appear.

Clean substitution patterns

A succinct approach is to place the sequence into Tuple and apply subs with a dictionary. If you already know which variables to fix, map them explicitly:

from sympy import Tuple as TPack
# Substitute only a known subset of symbols
TPack(*ans_vec).subs({s: 0 for s in (y2, y4, y5, y6, y7, y8)})
# Result: (-2.0, 2.0, 0, -2.0, 0, 0, 0, 0, 0)

If the set of free variables changes and you simply want to zero out everything that appears in the expression, use free_symbols from the wrapped object:

wrapped = TPack(*ans_vec)
wrapped.subs({t: 0 for t in wrapped.free_symbols})
# Result: (-2.0, 2.0, 0, -2.0, 0, 0, 0, 0, 0)

The replacement target does not have to be zero. It can be any object valid in the expression, such as an integer like 1 or another symbol. If you want an exact rational like one third, avoid writing 1/3 directly since that can turn into a decimal approximation; use S(1)/3 for an exact Rational.

from sympy import Symbol, S
z = Symbol('z')
wrapped.subs({t: z for t in wrapped.free_symbols})
wrapped.subs({t: S(1)/3 for t in wrapped.free_symbols})

Why this matters

With parametric solutions, the roster of free variables is not fixed. Building substitution maps by hand scales poorly and invites mistakes. Wrapping the solution in a subs-aware container and relying on free_symbols keeps the workflow compact and robust. It also makes it trivial to switch between zeroing parameters, plugging in integers, or inserting exact rationals where needed.

Takeaways

For post-processing linsolve outputs, treat the result as a SymPy expression by wrapping it into Tuple and substitute through subs. Either target a specific subset of symbols or use free_symbols when you want to blanket-replace everything that appears. If you need exact rationals, prefer S(1)/3 over 1/3 to avoid unintended decimal approximations.

The article is based on a question from StackOverflow by some1fromhell and an answer by smichr.