Many 'obvious' simplifications are false for complex numbers; assumptions are how you tell SymPy what you know.
By default a SymPy symbol is a complex number with no known properties. That is why sqrt(x**2) does not simplify to x (it is false for x = -1) and why log(a*b) is not expanded into log(a) + log(b).
You attach assumptions when creating the symbol: symbols("x", positive=True), real=True, integer=True, nonnegative=True, and so on. SymPy then uses them everywhere: in automatic evaluation, simplify, solveset, integrals (removing Piecewise conditions) and more.
You can query properties with attributes like expr.is_positive, expr.is_real or expr.is_integer. These use three-valued logic: they return True, False or None, where None means "cannot be decided". Never write if expr.is_positive: without thinking about None, which is falsy.
Assumptions are part of a symbol's identity: Symbol("x") and Symbol("x", positive=True) are different symbols, and substituting one for the other is a common source of "why didn't it simplify?" bugs. Keep one definition per name.
There is also a newer query system, ask(Q.positive(x), Q.positive(x) & ...), and refine(expr, Q.positive(x)), which lets you apply assumptions to an existing expression without re-creating symbols.
Example
from sympy import symbols, sqrt, simplify, log, expand_log, refine, Q, Abs, solveset
x, y = symbols("x y")
p = symbols("p", positive=True)
r = symbols("r", real=True)
print(sqrt(x**2), sqrt(p**2), sqrt(r**2)) # sqrt(x**2) p Abs(r)
q = symbols("q", positive=True)
print(expand_log(log(p * q))) # log(p) + log(q)
print(expand_log(log(x * y))) # log(x*y)
print((p + 1).is_positive) # True
print((x + 1).is_positive) # None (unknown, not False!)
print(refine(Abs(x), Q.positive(x))) # x
print(solveset(x**2 - 4, x)) # {-2, 2}
print(symbols("x") == symbols("x", positive=True)) # FalseRemember
- Plain symbols are complex; add positive=True, real=True etc. when true
- is_* attributes return True, False or None (unknown)
- A symbol with assumptions is a different symbol from one without
Watch out for
- Treating is_positive == None as False in an if statement.
- Mixing x = Symbol('x') and x = Symbol('x', real=True) in one computation.