2025, Oct 18 10:00
SymPy gotchas: parsing with assumptions and why Abs(cos(x))/sqrt(cos(x)**2) simplifies to 1
Learn why SymPy parse_expr ignores symbol assumptions and how to fix it: bind names via local_dict and use expression subs so Abs(cos(x))/sqrt(cos(x)**2) = 1.
When symbolic math libraries behave “too literally”, it often comes down to missing assumptions or mismatched symbols. A common case in SymPy is expecting Abs(cos(x))/sqrt(cos(x)**2) to simplify to 1, but getting the original expression back. The root cause and the fix are both straightforward once you know where to look.
Reproducing the issue
The following snippet constructs a symbol with assumptions and then parses a string expression. The expectation is to see 1, but the expression doesn’t simplify and a direct substitution by strings does nothing.
from sympy import Symbol, parse_expr, simplify
ang = Symbol('ang', positive=True, real=True)
src = '1*Abs(cos(ang))/sqrt(cos(ang)**2)'
expr = parse_expr(src)
failed = expr.subs("Abs(cos(ang))/sqrt(cos(ang)**2)", "1")
# expr still looks like Abs(cos(ang))/sqrt(cos(ang)**2)
# failed is unchanged, no replacement happened
Why the simplification does not happen
The key is that parse_expr does not know about your already-declared symbol unless you tell it. It sees the name ang in the string and silently creates a new Symbol("ang") with no assumptions attached. That newly created symbol is not real by default, and with no assumptions the expression Abs(cos(ang))/sqrt(cos(ang)**2) cannot be reduced to 1. By contrast, x/x simplifies even for complex x, so that case behaves differently.
There is a second pitfall in the replacement call. When subs receives a string as the first argument, it gets converted into a single symbol, not an expression tree. Your expression does not contain that single symbol, so nothing is replaced. It is safer to pass SymPy expressions or numbers to subs.
Two related observations help explain the behavior. First, the identity sqrt(x**2) == Abs(x) only holds when x is real; with general complex symbols, that equivalence is not valid because of the branch cut in sqrt. Second, by default SymPy symbols are complex unless you state assumptions such as real=True or positive=True, so simplifications that rely on real-ness will not trigger automatically.
The fix: bind your existing symbol to the parser
Provide local_dict to parse_expr so it reuses the already-declared symbol with its assumptions. Once you do that, the expression evaluates to 1 as expected. Also, perform substitutions using expressions rather than strings.
from sympy import Symbol, parse_expr
ang = Symbol('ang', positive=True, real=True)
src = '1*Abs(cos(ang))/sqrt(cos(ang)**2)'
expr_ok = parse_expr(src, local_dict={'ang': ang})
# expr_ok is 1
# If you specifically want a substitution pattern, pass expressions, not strings
pattern = parse_expr('Abs(cos(ang))/sqrt(cos(ang)**2)', local_dict={'ang': ang})
replaced = expr_ok.subs(pattern, 1)
# replaced is 1
If you need to avoid automatic evaluation during parsing for any reason, parse_expr can be called with evaluate=False, but this is not required for the fix above.
Why this detail matters
Algebraic equivalence checks, rule-based transformations, and automated simplifications all depend on consistent symbols and correct domain assumptions. If the parser invents a fresh symbol without assumptions, real-analytic identities won’t apply and equivalence checks can fail silently. Likewise, mixing strings into subs leads to non-matching placeholders that look syntactically similar yet never hit.
Takeaways
Bind names explicitly when parsing so the resulting expression reuses your existing symbols and their assumptions. Mark symbols as real or positive when you rely on identities that are only valid over the reals. Use subs with SymPy expressions, not strings, to ensure structural matching. With those safeguards in place, expressions like Abs(cos(x))/sqrt(cos(x)**2) simplify to 1 exactly when they should, and your symbolic workflows remain predictable.