Assumptions#

This page outlines the core assumptions system in SymPy. It explains what the core assumptions system is, how the assumptions system is used and what the different assumptions predicates mean.

Note

This page describes the core assumptions system also often referred to as the “old assumptions” system. There is also a “new assumptions” system which is described elsewhere. Note that the system described here is actually the system that is widely used in SymPy. The “new assumptions” system is not really used anywhere in SymPy yet and the “old assumptions” system will not be removed. At the time of writing (SymPy 1.7) it is still recommended for users to use the old assumption system.

Firstly we consider what happens when taking the square root of the square of a concrete integer such as \(2\) or \(-2\):

>>> from sympy import sqrt
>>> sqrt(2**2)
2
>>> sqrt((-2)**2)
2
>>> x = 2
>>> sqrt(x**2)
2
>>> sqrt(x**2) == x
True
>>> y = -2
>>> sqrt(y**2) == y
False
>>> sqrt(y**2) == -y
True

What these examples demonstrate is that for a positive number \(x\) we have \(\sqrt{x^2} = x\) whereas for a negative number we would instead have \(\sqrt{x^2} = -x\). That may seem obvious but the situation can be more surprising when working with a symbol rather then an explicit number. For example

>>> from sympy import Symbol, simplify
>>> x = Symbol('x')
>>> sqrt(x**2)
sqrt(x**2)

It might look as if that should simplify to x but it does not even if simplify() is used:

>>> simplify(sqrt(x**2))
sqrt(x**2)

This is because SymPy will refuse to simplify this expression if the simplification is not valid for every possible value of x. By default the symbol x is considered only to represent something roughly like an arbitrary complex number and the obvious simplification here is only valid for positive real numbers. Since x is not known to be positive or even real no simplification of this expression is possible.

We can tell SymPy that a symbol represents a positive real number when creating the symbol and then the simplification will happen automatically:

>>> y = Symbol('y', positive=True)
>>> sqrt(y**2)
y

This is what is meant by “assumptions” in SymPy. If the symbol y is created with positive=True then SymPy will assume that it represents a positive real number rather than an arbitrary complex or possibly infinite number. That assumption can make it possible to simplify expressions or might allow other manipulations to work. It is usually a good idea to be as precise as possible about the assumptions on a symbol when creating it.

The (old) assumptions system#

There are two sides to the assumptions system. The first side is that we can declare assumptions on a symbol when creating the symbol. The other side is that we can query the assumptions on any expression using the corresponding is_* attribute. For example:

>>> x = Symbol('x', positive=True)
>>> x.is_positive
True

We can query assumptions on any expression not just a symbol:

>>> x = Symbol('x', positive=True)
>>> expr = 1 + x**2
>>> expr
x**2 + 1
>>> expr.is_positive
True
>>> expr.is_negative
False

The values given in an assumptions query use three-valued “fuzzy” logic. Any query can return True, False, or None where None should be interpreted as meaning that the result is unknown.

>>> x = Symbol('x')
>>> y = Symbol('y', positive=True)
>>> z = Symbol('z', negative=True)
>>> print(x.is_positive)
None
>>> print(y.is_positive)
True
>>> print(z.is_positive)
False

Note

We need to use print in the above examples because the special value None does not display by default in the Python interpreter.

There are several reasons why an assumptions query might give None. It is possible that the query is unknowable as in the case of x above. Since x does not have any assumptions declared it roughly represents an arbitrary complex number. An arbitrary complex number might be a positive real number but it also might not be. Without further information there is no way to resolve the query x.is_positive.

Another reason why an assumptions query might give None is that there does in many cases the problem of determining whether an expression is e.g. positive is undecidable. That means that there does not exist an algorithm for answering the query in general. For some cases an algorithm or at least a simple check would be possible but has not yet been implemented although it could be added to SymPy.

The final reason that an assumptions query might give None is just that the assumptions system does not try very hard to answer complicated queries. The system is intended to be fast and uses simple heuristic methods to conclude a True or False answer in common cases. For example any sum of positive terms is positive so:

>>> from sympy import symbols
>>> x, y = symbols('x, y', positive=True)
>>> expr = x + y
>>> expr
x + y
>>> expr.is_positive
True

The last example is particularly simple so the assumptions system is able to give a definite answer. If the sum involved a mix of positive or negative terms it would be a harder query:

>>> x = Symbol('x', real=True)
>>> expr = 1 + (x - 2)**2
>>> expr
(x - 2)**2 + 1
>>> expr.is_positive
True
>>> expr2 = expr.expand()
>>> expr2
x**2 - 4*x + 5
>>> print(expr2.is_positive)
None

Ideally that last example would give True rather than None because the expression is always positive for any real value of x (and x has been assumed real). The assumptions system is intended to be efficient though: it is expected many more complex queries will not be fully resolved. This is because assumptions queries are primarily used internally by SymPy as part of low-level calculations. Making the system more comprehensive would slow SymPy down.

Note that in fuzzy logic giving an indeterminate result None is never a contradiction. If it is possible to infer a definite True or False result when resolving a query then that is better than returning None. However a result of None is not a bug. Any code that uses the assumptions system needs to be prepared to handle all three cases for any query and should not presume that a definite answer will always be given.

The assumptions system is not just for symbols or for complex expressions. It can also be used for plain SymPy integers and other objects. The assumptions predicates are available on any instance of Basic which is the superclass for most classes of SymPy objects. A plain Python int is not a Basic instance and can not be used to query assumptions predicates. We can “sympify” regular Python objects to become SymPy objects with sympify() or S (SingletonRegistry) and then the assumptions system can be used:

>>> from sympy import S
>>> x = 2
>>> x.is_positive
Traceback (most recent call last):
...
AttributeError: 'int' object has no attribute 'is_positive'
>>> x = S(2)
>>> type(x)
<class 'sympy.core.numbers.Integer'>
>>> x.is_positive
True

Gotcha: symbols with different assumptions#

In SymPy it is possible to declare two symbols with different names and they will implicitly be considered equal under structural equality:

>>> x1 = Symbol('x')
>>> x2 = Symbol('x')
>>> x1
x
>>> x2
x
>>> x1 == x2
True

However if the symbols have different assumptions then they will be considered to represent distinct symbols:

>>> x1 = Symbol('x', positive=True)
>>> x2 = Symbol('x')
>>> x1
x
>>> x2
x
>>> x1 == x2
False

One way to simplify an expression is to use the posify() function which will replace all symbols in an expression with symbols that have the assumption positive=True (unless that contradicts any existing assumptions for the symbol):

>>> from sympy import posify, exp
>>> x = Symbol('x')
>>> expr = exp(sqrt(x**2))
>>> expr
exp(sqrt(x**2))
>>> posify(expr)
(exp(_x), {_x: x})
>>> expr2, rep = posify(expr)
>>> expr2
exp(_x)

The posify() function returns the expression with all symbols replaced (which might lead to simplifications) and also a dict which maps the new symbols to the old that can be used with subs(). This is useful because otherwise the new expression with the new symbols having the positive=True assumption will not compare equal to the old:

>>> expr2
exp(_x)
>>> expr2 == exp(x)
False
>>> expr2.subs(rep)
exp(x)
>>> expr2.subs(rep) == exp(x)
True

Applying assumptions to string inputs#

We have seen how to set assumptions when Symbol or symbols() explicitly. A natural question to ask is in what other situations can we assign assumptions to an object?

It is common for users to use strings as input to SymPy functions (although the general feeling among SymPy developers is that this should be discouraged) e.g.:

>>> from sympy import solve
>>> solve('x**2 - 1')
[-1, 1]

When creating symbols explicitly it would be possible to assign assumptions that would affect the behaviour of solve():

>>> x = Symbol('x', positive=True)
>>> solve(x**2 - 1)
[1]

When using string input SymPy will create the expression and create all of the symbolc implicitly so the question arises how can the assumptions be specified? The answer is that rather than depending on implicit string conversion it is better to use the parse_expr() function explicitly and then it is possible to provide assumptions for the symbols e.g.:

>>> from sympy import parse_expr
>>> parse_expr('x**2 - 1')
x**2 - 1
>>> eq = parse_expr('x**2 - 1', {'x':Symbol('x', positive=True)})
>>> solve(eq)
[1]

Note

The solve() function is unusual as a high level API in that it actually checks the assumptions on any input symbols (the unknowns) and uses that to tailor its output. The assumptions system otherwise affects low-level evaluation but is not necessarily handled explicitly by high-level APIs.

Predicates#

There are many different predicates that can be assumed for a symbol or can be queried for an expression. It is possible to combine multiple predicates when creating a symbol. Predicates are logically combined using and so if a symbol is declared with positive=True and also with integer=True then it is both positive and integer:

>>> x = Symbol('x', positive=True, integer=True)
>>> x.is_positive
True
>>> x.is_integer
True

The full set of known predicates for a symbol can be accessed using the assumptions0 attribute:

>>> x.assumptions0
{'algebraic': True,
 'commutative': True,
 'complex': True,
 'extended_negative': False,
 'extended_nonnegative': True,
 'extended_nonpositive': False,
 'extended_nonzero': True,
 'extended_positive': True,
 'extended_real': True,
 'finite': True,
 'hermitian': True,
 'imaginary': False,
 'infinite': False,
 'integer': True,
 'irrational': False,
 'negative': False,
 'noninteger': False,
 'nonnegative': True,
 'nonpositive': False,
 'nonzero': True,
 'positive': True,
 'rational': True,
 'real': True,
 'transcendental': False,
 'zero': False}

We can see that there are many more predicates listed than the two that were used to create x. This is because the assumptions system can infer some predicates from combinations of other predicates. For example if a symbol is declared with positive=True then it is possible to infer that it should have negative=False because a positive number can never be negative. Similarly if a symbol is created with integer=True then it is possible to infer that is should have rational=True because every integer is a rational number.

A full table of the possible predicates and their definitions is given below.

Assumptions predicates for the (old) assumptions#

Predicate

Definition

Implications

commutative

A commutative expression. A commutative expression commutes with all other expressions under multiplication. If an expression a has commutative=True then a * b == b * a for any other expression b (even if b is not commutative). Unlike all other assumptions predicates commutative must always be True or False and can never be None. Also unlike all other predicates commutative defaults to True in e.g. Symbol('x'). [commutative]

infinite

An infinite expression such as oo, -oo or zoo. [infinite]

== !finite

finite

A finite expression. Any expression that is not infinite is considered finite. [infinite]

== !infinite

hermitian

An element of the field of Hermitian operators. [antihermitian]

antihermitian

An element of the field of antihermitian operators. [antihermitian]

complex

A complex number, \(z\in\mathbb{C}\). Any number of the form \(x + iy\) where \(x\) and \(y\) are real and \(i = \sqrt{-1}\). All complex numbers are finite. Includes all real numbers. [complex]

-> commutative
-> finite

algebraic

An algebraic number, \(z\in\overline{\mathbb{Q}}\). Any number that is a root of a non-zero polynomial \(p(z)\in\mathbb{Q}[z]\) having rational coefficients. All algebraic numbers are complex. An algebraic number may or may not be real. Includes all rational numbers. [algebraic]

-> complex

transcendental

A complex number that is not algebraic, \(z\in\mathbb{C}-\overline{\mathbb{Q}}\). All transcendental numbers are complex. A transcendental number may or may not be real but can never be rational. [transcendental]

== (complex & !algebraic)

extended_real

An element of the extended real number line, \(x\in\overline{\mathbb{R}}\) where \(\overline{\mathbb{R}}=\mathbb{R}\cup\{-\infty,+\infty\}\). An extended_real number is either real or \(\pm\infty\). The relational operators <, <=, >= and > are defined only for expressions that are extended_real. [extended_real]

-> commutative

real

A real number, \(x\in\mathbb{R}\). All real numbers are finite and complex (the set of reals is a subset of the set of complex numbers). Includes all rational numbers. A real number is either negative, zero or positive. [real]

-> complex
== (extended_real & finite)
== (negative | zero | positive)
-> hermitian

imaginary

An imaginary number, \(z\in\mathbb{I}-\{0\}\). A number of the form \(z=yi\) where \(y\) is real, \(y\ne 0\) and \(i=\sqrt{-1}\). All imaginary numbers are complex and not real. Note in particular that zero is \(not\) considered imaginary in SymPy. [imaginary]

-> complex
-> antihermitian
-> !extended_real

rational

A rational number, \(q\in\mathbb{Q}\). Any number of the form \(\frac{a}{b}\) where \(a\) and \(b\) are integers and \(b \ne 0\). All rational numbers are real and algebraic. Includes all integer numbers. [rational]

-> real
-> algebraic

irrational

A real number that is not rational, \(x\in\mathbb{R}-\mathbb{Q}\). [irrational]

== (real & !rational)

integer

An integer, \(a\in\mathbb{Z}\). All integers are rational. Includes zero and all prime, composite, even and odd numbers. [integer]

-> rational

noninteger

An extended real number that is not an integer, \(x\in\overline{\mathbb{R}}-\mathbb{Z}\).

== (extended_real & !integer)

even

An even number, \(e\in\{2k: k\in\mathbb{Z}\}\). All even numbers are integer numbers. Includes zero. [parity]

-> integer
-> !odd

odd

An odd number, \(o\in\{2k + 1: k\in\mathbb{Z}\}\). All odd numbers are integer numbers. [parity]

-> integer
-> !even

prime

A prime number, \(p\in\mathbb{P}\). All prime numbers are positive and integer. [prime]

-> integer
-> positive

composite

A composite number, \(c\in\mathbb{N}-(\mathbb{P}\cup\{1\})\). A positive integer that is the product of two or more primes. A composite number is always a positive integer and is not prime. [composite]

-> (integer & positive & !prime)
!composite -> (!positive | !even | prime)

zero

The number \(0\). An expression with zero=True represents the number 0 which is an integer. [zero]

-> even & finite
== (extended_nonnegative & extended_nonpositive)
== (nonnegative & nonpositive)

nonzero

A nonzero real number, \(x\in\mathbb{R}-\{0\}\). A nonzero number is always real and can not be zero.

-> real
== (extended_nonzero & finite)

extended_nonzero

A member of the extended reals that is not zero, \(x\in\overline{\mathbb{R}}-\{0\}\).

== (extended_real & !zero)

positive

A positive real number, \(x\in\mathbb{R}, x>0\). All positive numbers are finite so oo is not positive. [positive]

== (nonnegative & nonzero)
== (extended_positive & finite)

nonnegative

A nonnegative real number, \(x\in\mathbb{R}, x\ge 0\). All nonnegative numbers are finite so oo is not nonnegative. [positive]

== (real & !negative)
== (extended_nonnegative & finite)

negative

A negative real number, \(x\in\mathbb{R}, x<0\). All negative numbers are finite so -oo is not negative. [negative]

== (nonpositive & nonzero)
== (extended_negative & finite)

nonpositive

A nonpositive real number, \(x\in\mathbb{R}, x\le 0\). All nonpositive numbers are finite so -oo is not nonpositive. [negative]

== (real & !positive)
== (extended_nonpositive & finite)

extended_positive

A positive extended real number, \(x\in\overline{\mathbb{R}}, x>0\). An extended_positive number is either positive or oo. [extended_real]

== (extended_nonnegative & extended_nonzero)

extended_nonnegative

A nonnegative extended real number, \(x\in\overline{\mathbb{R}}, x\ge 0\). An extended_nonnegative number is either nonnegative or oo. [extended_real]

== (extended_real & !extended_negative)

extended_negative

A negative extended real number, \(x\in\overline{\mathbb{R}}, x<0\). An extended_negative number is either negative or -oo. [extended_real]

== (extended_nonpositive & extended_nonzero)

extended_nonpositive

A nonpositive extended real number, \(x\in\overline{\mathbb{R}}, x\le 0\). An extended_nonpositive number is either nonpositive or -oo. [extended_real]

== (extended_real & !extended_positive)

References for the above definitions#

Implications#

The assumptions system uses the inference rules to infer new predicates beyond those immediately specified when creating a symbol:

>>> x = Symbol('x', real=True, negative=False, zero=False)
>>> x.is_positive
True

Although x was not explicitly declared positive it can be inferred from the predicates that were given explicitly. Specifically one of the inference rules is real == negative | zero | positive so if real is True and both negative and zero are False then positive must be True.

In practice the assumption inference rules mean that it is not necessary to include redundant predicates for example a positive real number can be simply be declared as positive:

>>> x1 = Symbol('x1', positive=True, real=True)
>>> x2 = Symbol('x2', positive=True)
>>> x1.is_real
True
>>> x2.is_real
True
>>> x1.assumptions0 == x2.assumptions0
True

Combining predicates that are inconsistent will give an error:

>>> x = Symbol('x', commutative=False, real=True)
Traceback (most recent call last):
...
InconsistentAssumptions: {
      algebraic: False,
      commutative: False,
      complex: False,
      composite: False,
      even: False,
      extended_negative: False,
      extended_nonnegative: False,
      extended_nonpositive: False,
      extended_nonzero: False,
      extended_positive: False,
      extended_real: False,
      imaginary: False,
      integer: False,
      irrational: False,
      negative: False,
      noninteger: False,
      nonnegative: False,
      nonpositive: False,
      nonzero: False,
      odd: False,
      positive: False,
      prime: False,
      rational: False,
      real: False,
      transcendental: False,
      zero: False}, real=True

Interpretation of the predicates#

Although the predicates are defined in the table above it is worth taking some time to think about how to interpret them. Firstly many of the concepts referred to by the predicate names like “zero”, “prime”, “rational” etc have a basic meaning in mathematics but can also have more general meanings. For example when dealing with matrices a matrix of all zeros might be referred to as “zero”. The predicates in the assumptions system do not allow any generalizations such as this. The predicate zero is strictly reserved for the plain number \(0\). Instead matrices have an is_zero_matrix() property for this purpose (although that property is not strictly part of the assumptions system):

>>> from sympy import Matrix
>>> M = Matrix([[0, 0], [0, 0]])
>>> M.is_zero
False
>>> M.is_zero_matrix
True

Similarly there are generalisations of the integers such as the Gaussian integers which have a different notion of prime number. The prime predicate in the assumptions system does not include those and strictly refers only to the standard prime numbers \(\mathbb{P} = \{2, 3, 5, 7, 11, \cdots\}\). Likewise integer only means the standard concept of the integers \(\mathbb{Z} = \{0, \pm 1, \pm 2, \cdots\}\), rational only means the standard concept of the rational numbers \(\mathbb{Q}\) and so on.

The predicates set up schemes of subsets such as the chain beginning with the complex numbers which are considered as a superset of the reals which are in turn a superset of the rationals and so on. The chain of subsets

\[\mathbb{Z} \subset \mathbb{Q} \subset \mathbb{R} \subset \mathbb{C}\]

corresponds to the chain of implications in the assumptions system

integer -> rational -> real -> complex

A “vanilla” symbol with no assumptions explicitly attached is not known to belong to any of these sets and is not even known to be finite:

>>> x = Symbol('x')
>>> x.assumptions0
{'commutative': True}
>>> print(x.is_commutative)
True
>>> print(x.is_rational)
None
>>> print(x.is_complex)
None
>>> print(x.is_real)
None
>>> print(x.is_integer)
None
>>> print(x.is_finite)
None

It is hard for SymPy to know what it can do with such a symbol that is not even known to be finite or complex so it is generally better to give some assumptions to the symbol explicitly. Many parts of SymPy will implicitly treat such a symbol as complex and in some cases SymPy will permit manipulations that would not strictly be valid given that x is not known to be finite. In a formal sense though very little is known about a vanilla symbol which makes manipulations involving it difficult.

Defining something about a symbol can make a big difference. For example if we declare the symbol to be an integer then this implies a suite of other predicates that will help in further manipulations:

>>> n = Symbol('n', integer=True)
>>> n.assumptions0
{'algebraic': True,
 'commutative': True,
 'complex': True,
 'extended_real': True,
 'finite': True,
 'hermitian': True,
 'imaginary': False,
 'infinite': False,
 'integer': True,
 'irrational': False,
 'noninteger': False,
 'rational': True,
 'real': True,
 'transcendental': False}

These assumptions can lead to very significant simplifications e.g. integer=True gives:

>>> from sympy import sin, pi
>>> n1 = Symbol('n1')
>>> n2 = Symbol('n2', integer=True)
>>> sin(n1 * pi)
sin(pi*n1)
>>> sin(n2 * pi)
0

Replacing a whole expression with \(0\) is about as good as simplification can get!

It is normally advisable to set as many assumptions as possible on any symbols so that expressions can be simplified as much as possible. A common misunderstanding leads to defining a symbol with a False predicate e.g.:

>>> x = Symbol('x', negative=False)
>>> print(x.is_negative)
False
>>> print(x.is_nonnegative)
None
>>> print(x.is_real)
None
>>> print(x.is_complex)
None
>>> print(x.is_finite)
None

If the intention is to say that x is a real number that is not positive then that needs to be explicitly stated. In the context that the symbol is known to be real, the predicate positive=False becomes much more meaningful:

>>> x = Symbol('x', real=True, negative=False)
>>> print(x.is_negative)
False
>>> print(x.is_nonnegative)
True
>>> print(x.is_real)
True
>>> print(x.is_complex)
True
>>> print(x.is_finite)
True

A symbol declared as Symbol('x', real=True, negative=False) is equivalent to a symbol declared as Symbol('x', nonnegative=True). Simply declaring a symbol as Symbol('x', positive=False) does not allow the assumptions system to conclude much about it because a vanilla symbol is not known to be finite or even complex.

A related confusion arises with Symbol('x', complex=True) and Symbol('x', real=False). Often when either of these is used neither is what is actually wanted. The first thing to understand is that all real numbers are complex so a symbol created with real=True will also have complex=True and a symbol created with complex=True will not have real=False. If the intention was to create a complex number that is not a real number then it should be Symbol('x', complex=True, real=False). On the other hand declaring real=False alone is not sufficient to conclude that complex=True because knowing that it is not a real number does not tell us whether it is finite or whether or not it is some completely different kind of object from a complex number.

A vanilla symbol is defined by not knowing whether it is finite etc but there is no clear definition of what it should actually represent. It is tempting to think of it as an “arbitrary complex number or possibly one of the infinities” but there is no way to query an arbitrary (non-symbol) expression in order to determine if it meets those criteria. It is important to bear in mind that within the SymPy codebase and potentially in downstream libraries many other kinds of mathematical objects can be found that might also have commutative=True while being something very different from an ordinary number (in this context even SymPy’s standard infinities are considered “ordinary”).

The only predicate that is applied by default for a symbol is commutative. We can also declare a symbol to be noncommutative e.g.:

>>> x, y = symbols('x, y', commutative=False)
>>> z = Symbol('z')  # defaults to commutative=True
>>> x*y + y*x
x*y + y*x
>>> x*z + z*x
2*z*x

Note here that since x and y are both noncommutative x and y do not commute so x*y != y*x. On the other hand since z is commutative x and z commute and x*z == z*x even though x is noncommutative.

The interpretation of what a vanilla symbol represents is unclear but the interpretation of an expression with commutative=False is entirely obscure. Such an expression is necessarily not a complex number or an extended real or any of the standard infinities (even zoo is commutative). We are left with very little that we can say about what such an expression does represent.

Other is_* properties#

There are many properties and attributes in SymPy that that have names beginning with is_ that look similar to the properties used in the (old) assumptions system but are not in fact part of the assumptions system. Some of these have a similar meaning and usage as those of the assumptions system such as the is_zero_matrix() property shown above. Another example is the is_empty property of sets:

>>> from sympy import FiniteSet, Intersection
>>> S1 = FiniteSet(1, 2)
>>> S1
{1, 2}
>>> print(S1.is_empty)
False
>>> S2 = Intersection(FiniteSet(1), FiniteSet(Symbol('x')))
>>> S2
Intersection({1}, {x})
>>> print(S2.is_empty)
None

The is_empty property gives a fuzzy-bool indicating whether or not a Set is the empty set. In the example of S2 it is not possible to know whether or not the set is empty without knowing whether or not x is equal to 1 so S2.is_empty gives None. The is_empty property for sets plays a similar role to the is_zero property for numbers in the assumptions system: is_empty is normally only True for the EmptySet object but it is still useful to be able to distinguish between the cases where is_empty=False and is_empty=None.

Although is_zero_matrix and is_empty are used for similar purposes to the assumptions properties such as is_zero they are not part of the (old) assumptions system. There are no associated inference rules connecting e.g. Set.is_empty and Set.is_finite_set because the inference rules are part of the (old) assumptions system which only deals with the predicates listed in the table above. It is not possible to declare a MatrixSymbol with e.g. zero_matrix=False and there is no SetSymbol class but if there was it would not have a system for understanding predicates like empty=False.

The properties is_zero_matrix() and is_empty are similar to those of the assumptions system because they concern semantic aspects of an expression. There are a large number of other properties that focus on structural aspects such as is_Number, is_number(), is_comparable(). Since these properties refer to structural aspects of an expression they will always give True or False rather than a fuzzy bool that also has the possibility of being None. Capitalised properties such as is_Number are usually shorthand for isinstance checks e.g.:

>>> from sympy import Number, Rational
>>> x = Rational(1, 2)
>>> isinstance(x, Number)
True
>>> x.is_Number
True
>>> y = Symbol('y', rational=True)
>>> isinstance(y, Number)
False
>>> y.is_Number
False

The Number class is the superclass for Integer, Rational and Float so any instance of Number represents a concrete number with a known value. A symbol such as y that is declared with rational=True might represent the same value as x but it is not a concrete number with a known value so this is a structural rather than a semantic distinction. Properties like is_Number are sometimes used in SymPy in place of e.g. isinstance(obj, Number) because they do not have problems with circular imports and checking x.is_Number can be faster than a call to isinstance.

The is_number (lower-case) property is very different from is_Number. The is_number property is True for any expression that can be numerically evaluated to a floating point complex number with evalf():

>>> from sympy import I
>>> expr1 = I + sqrt(2)
>>> expr1
sqrt(2) + I
>>> expr1.is_number
True
>>> expr1.evalf()
1.4142135623731 + 1.0*I
>>> x = Symbol('x')
>>> expr2 = 1 + x
>>> expr2
x + 1
>>> expr2.is_number
False
>>> expr2.evalf()
x + 1.0

The primary reason for checking expr.is_number is to predict whether a call to evalf() will fully evaluate. The is_comparable() property is similar to is_number() except that if is_comparable gives True then the expression is guaranteed to numerically evaluate to a real Float. When a.is_comparable and b.is_comparable the inequality a < b should be resolvable as something like a.evalf() < b.evalf().

The full set of is_* properties, attributes and methods in SymPy is large. It is important to be clear though that only those that are listed in the table of predicates above are actually part of the assumptions system. It is only those properties that are involved in the mechanism that implements the assumptions system which is explained below.

Implementing assumptions handlers#

We will now work through an example of how to implement a SymPy symbolic function so that we can see how the old assumptions are used internally. SymPy already has an exp function which is defined for all complex numbers but we will define an expreal function which is restricted to real arguments.

>>> from sympy import Function
>>> from sympy.core.logic import fuzzy_and, fuzzy_or
>>>
>>> class expreal(Function):
...     """exponential function E**x restricted to the extended reals"""
...
...     is_extended_nonnegative = True
...
...     @classmethod
...     def eval(cls, x):
...         # Validate the argument
...         if x.is_extended_real is False:
...             raise ValueError("non-real argument to expreal")
...         # Evaluate for special values
...         if x.is_zero:
...             return S.One
...         elif x.is_infinite:
...             if x.is_extended_negative:
...                 return S.Zero
...             elif x.is_extended_positive:
...                 return S.Infinity
...
...     @property
...     def x(self):
...         return self.args[0]
...
...     def _eval_is_finite(self):
...         return fuzzy_or([self.x.is_real, self.x.is_extended_nonpositive])
...
...     def _eval_is_algebraic(self):
...         if fuzzy_and([self.x.is_rational, self.x.is_nonzero]):
...             return False
...
...     def _eval_is_integer(self):
...         if self.x.is_zero:
...             return True
...
...     def _eval_is_zero(self):
...         return fuzzy_and([self.x.is_infinite, self.x.is_extended_negative])

The Function.eval method is used to pick up on special values of the function so that we can return a different object if it would be a simplification. When expreal(x) is called the expreal.__new__ class method (defined in the superclass Function) will call expreal.eval(x). If expreal.eval returns something other than None then that will be returned instead of an unevaluated expreal(x):

>>> from sympy import oo
>>> expreal(1)
expreal(1)
>>> expreal(0)
1
>>> expreal(-oo)
0
>>> expreal(oo)
oo

Note that the expreal.eval method does not compare the argument using ==. The special values are verified using the assumptions system to query the properties of the argument. That means that the expreal method can also evaluate for different forms of expression that have matching properties e.g.

>>> x = Symbol('x', extended_negative=True, infinite=True)
>>> x
x
>>> expreal(x)
0

Of course the assumptions system can only resolve a limited number of special values so most eval methods will also check against some special values with == but it is preferable to check e.g. x.is_zero rather than x==0.

Note also that the expreal.eval method validates that the argument is real. We want to allow \(\pm\infty\) as arguments to expreal so we check for extended_real rather than real. If the argument is not extended real then we raise an error:

>>> expreal(I)
Traceback (most recent call last):
...
ValueError: non-real argument to expreal

Importantly we check x.is_extended_real is False rather than not x.is_extended_real which means that we only reject the argument if it is definitely not extended real: if x.is_extended_real gives None then the argument will not be rejected. The first reason for allowing x.is_extended_real=None is so that a vanilla symbol can be used with expreal. The second reason is that an assumptions query can always give None even in cases where an argument is definitely real e.g.:

>>> x = Symbol('x')
>>> print(x.is_extended_real)
None
>>> expreal(x)
expreal(x)
>>> expr = (1 + I)/sqrt(2) + (1 - I)/sqrt(2)
>>> print(expr.is_extended_real)
None
>>> expr.expand()
sqrt(2)
>>> expr.expand().is_extended_real
True
>>> expreal(expr)
expreal(sqrt(2)*(1 - I)/2 + sqrt(2)*(1 + I)/2)

Validating the argument in expreal.eval does mean that it will not be validated when evaluate=False is passed but there is not really a better place to perform the validation:

>>> expreal(I, evaluate=False)
expreal(I)

The extended_nonnegative class attribute and the _eval_is_* methods on the expreal class implement queries in the assumptions system for instances of expreal:

>>> expreal(2)
expreal(2)
>>> expreal(2).is_finite
True
>>> expreal(2).is_integer
False
>>> expreal(2).is_rational
False
>>> expreal(2).is_algebraic
False
>>> z = expreal(-oo, evaluate=False)
>>> z
expreal(-oo)
>>> z.is_integer
True
>>> x = Symbol('x', real=True)
>>> expreal(x)
expreal(x)
>>> expreal(x).is_nonnegative
True

The assumptions system resolves queries like expreal(2).is_finite using the corresponding handler expreal._eval_is_finite and also the implication rules. For example it is known that expreal(2).is_rational is False because expreal(2)._eval_is_algebraic returns False and there is an implication rule rational -> algebraic. This means that an is_rational query can be resolved in this case by the _eval_is_algebraic handler. It is actually better not to implement assumptions handlers for every possible predicate but rather to try and identify a minimal set of handlers that can resolve as many queries as possible with as few checks as possible.

Another point to note is that the _eval_is_* methods only make assumptions queries on the argument x and do not make any assumptions queries on self. Recursive assumptions queries on the same object will interfere with the assumptions implications resolver potentially leading to non-deterministic behaviour so they should not be used (there are examples of this in the SymPy codebase but they should be removed).

Many of the expreal methods implicitly return None. This is a common pattern in the assumptions system. The eval method and the _eval_is_* methods can all return None and often will. A Python function that ends without reaching a return statement will implicitly return None. We take advantage of this by leaving out many of the else clauses from the if statements and allowing None to be returned implicitly. When following the control flow of these methods it is important to bear in mind firstly that any queried property can give True, False or None and also that any function will implicitly return None if all of the conditionals fail.

Mechanism of the assumptions system#

Note

This section describes internal details that could change in a future SymPy version.

This section will explain the inner workings of the assumptions system. It is important to understand that these inner workings are implementation details and could change from one SymPy version to another. This explanation is written as of SymPy 1.7. Although the (old) assumptions system has many limitations (discussed in the next section) it is a mature system that is used extensively in SymPy and has been well optimised for its current usage. The assumptions system is used implicitly in most SymPy operations to control evaluation of elementary expressions.

There are several stages in the implementation of the assumptions system within a SymPy process that lead up to the evaluation of a single query in the assumptions system. Briefly these are:

  1. At import time the assumptions rules defined in sympy/core/assumptions.py are processed into a canonical form ready for efficiently applying the implication rules. This happens once when SymPy is imported before even the Basic class is defined.

  2. The Basic.__init_subclass__ method will post-process every Basic subclass to add the relevant properties needed for assumptions queries. This also adds the default_assumptions attribute to the class. This happens each time a Basic subclass is defined (when its containing module is imported).

  3. Every Basic instance initially uses the default_assumptions class attribute. When an assumptions query is made on a Basic instance in the first instance the query will be answered from the default_assumptions for the class.

  4. If there is no cached value for the assumptions query in the default_assumptions for the class then the default assumptions will be copied to make an assumptions cache for the instance. Then the _ask() function is called to resolve the query which will firstly call the relevant instance handler _eval_is method. If the handler returns non-None then the result will be cached and returned.

  5. If the handler does not exist or gives None then the implications resolver is tried. This will enumerate (in a randomised order) all possible combinations of predicates that could potentially be used to resolve the query under the implication rules. In each case the handler _eval_is method will be called to see if it gives non-None. If any combination of handlers and implication rules leads to a definitive result for the query then that result is cached in the instance cache and returned.

  6. Finally if the implications resolver failed to resolve the query then the query is considered unresolvable. The value of None for the query is cached in the instance cache and returned.

The assumptions rules defined in sympy/core/assumptions.py are given in forms like real ==  negative | zero | positive. When this module is imported these are converted into a FactRules instance called _assume_rules. This preprocesses the implication rules into the form of “A” and “B” rules that can be used for the implications resolver. This is explained in the code in sympy/core/facts.py. We can access this internal object directly like (full output omitted):

>>> from sympy.core.assumptions import _assume_rules
>>> _assume_rules.defined_facts   
{'algebraic',
 'antihermitian',
 'commutative',
 'complex',
 'composite',
 'even',
 ...
>>> _assume_rules.full_implications   
defaultdict(set,
            {('extended_positive', False): {('composite', False),
  ('positive', False),
  ('prime', False)},
 ('finite', False): {('algebraic', False),
  ('complex', False),
  ('composite', False),
  ...

The Basic.__init_subclass__ method will inspect the attributes of each Basic class to see if any assumptions related attributes are defined. An example of these is the is_extended_nonnegative = True attribute defined in the expreal class. The implications of any such attributes will be used to precompute any statically knowable assumptions. For example is_extended_nonnegative=True implies real=True etc. A StdFactKB instance is created for the class which stores those assumptions whose values are known at this stage. The StdFactKB instance is assigned as the class attribute default_assumptions. We can see this with

>>> from sympy import Expr
...
>>> class A(Expr):
...     is_positive = True
...
...     def _eval_is_rational(self):
...         # Let's print something to see when this method is called...
...         print('!!! calling _eval_is_rational')
...         return True
...
>>> A.is_positive
True
>>> A.is_real  # inferred from is_positive
True

Although only is_positive was defined in the class A it also has attributes such as is_real which are inferred from is_positive. The set of all such assumptions for class A can be seen in default_assumptions which looks like a dict but is in fact a StdFactKB instance:

>>> type(A.default_assumptions)
<class 'sympy.core.assumptions.StdFactKB'>
>>> A.default_assumptions
{'commutative': True,
 'complex': True,
 'extended_negative': False,
 'extended_nonnegative': True,
 'extended_nonpositive': False,
 'extended_nonzero': True,
 'extended_positive': True,
 'extended_real': True,
 'finite': True,
 'hermitian': True,
 'imaginary': False,
 'infinite': False,
 'negative': False,
 'nonnegative': True,
 'nonpositive': False,
 'nonzero': True,
 'positive': True,
 'real': True,
 'zero': False}

When an instance of any Basic subclass is created Basic.__new__ will assign its _assumptions attribute which will initially be a reference to cls.default_assumptions shared amongst all instances of the same class. The instance will use this to resolve any assumptions queries until that fails to give a definitive result at which point a copy of cls.default_assumptions will be created and assigned to the instance’s _assumptions attribute. The copy will be used as a cache to store any results computed for the instance by its _eval_is handlers.

When the _assumptions attribute fails to give the relevant result it is time to call the _eval_is handlers. At this point the _ask() function is called. The _ask() function will initially try to resolve a query such as is_rational by calling the corresponding method i.e. _eval_is_rational. If that gives non-None then the result is stored in _assumptions and any implications of that result are computed and stored as well. At that point the query is resolved and the value returned.

>>> a = A()
>>> a._assumptions is A.default_assumptions
True
>>> a.is_rational
!!! calling _eval_is_rational
True
>>> a._assumptions is A.default_assumptions
False
>>> a._assumptions   # rational now shows as True
{'algebraic': True,
 'commutative': True,
 'complex': True,
 'extended_negative': False,
 'extended_nonnegative': True,
 'extended_nonpositive': False,
 'extended_nonzero': True,
 'extended_positive': True,
 'extended_real': True,
 'finite': True,
 'hermitian': True,
 'imaginary': False,
 'infinite': False,
 'irrational': False,
 'negative': False,
 'nonnegative': True,
 'nonpositive': False,
 'nonzero': True,
 'positive': True,
 'rational': True,
 'real': True,
 'transcendental': False,
 'zero': False}

If e.g. _eval_is_rational does not exist or gives None then _ask() will try all possibilities to use the implication rules and any other handler methods such as _eval_is_integer, _eval_is_algebraic etc that might possibly be able to give an answer to the original query. If any method leads to a definite result being known for the original query then that is returned. Otherwise once all possibilities for using a handler and the implication rules to resolve the query are exhausted None will be cached and returned.

>>> b = A()
>>> b.is_algebraic    # called _eval_is_rational indirectly
!!! calling _eval_is_rational
True
>>> c = A()
>>> print(c.is_prime)   # called _eval_is_rational indirectly
!!! calling _eval_is_rational
None
>>> c._assumptions   # prime now shows as None
{'algebraic': True,
 'commutative': True,
 'complex': True,
 'extended_negative': False,
 'extended_nonnegative': True,
 'extended_nonpositive': False,
 'extended_nonzero': True,
 'extended_positive': True,
 'extended_real': True,
 'finite': True,
 'hermitian': True,
 'imaginary': False,
 'infinite': False,
 'irrational': False,
 'negative': False,
 'nonnegative': True,
 'nonpositive': False,
 'nonzero': True,
 'positive': True,
 'prime': None,
 'rational': True,
 'real': True,
 'transcendental': False,
 'zero': False}

Note

In the _ask() function the handlers are called in a randomised order which can mean that execution at this point is non-deterministic. Provided all of the different handler methods are consistent (i.e. there are no bugs) then the end result will still be deterministic. However a bug where two handlers are inconsistent can manifest in non-deterministic behaviour because this randomisation might lead to the handlers being called in different orders when the same program is run multiple times.

Limitations#

Combining predicates with or#

In the old assumptions we can easily combine predicates with and when creating a Symbol e.g.:

>>> x = Symbol('x', integer=True, positive=True)
>>> x.is_positive
True
>>> x.is_integer
True

We can also easily query whether two conditions are jointly satisfied with

>>> fuzzy_and([x.is_positive, x.is_integer])
True
>>> x.is_positive and x.is_integer
True

However there is no way in the old assumptions to create a Symbol with assumptions predicates combined with or. For example if we wanted to say that “x is positive or x is an integer” then it is not possible to create a Symbol with those assumptions.

It is also not possible to ask an assumptions query based on or e.g. “is expr an expression that is positive or an integer”. We can use e.g.

>>> fuzzy_or([x.is_positive, x.is_integer])
True

However if all that is known about x is that it is possibly positive or otherwise a negative integer then both queries x.is_positive and x.is_integer will resolve to None. That means that the query becomes

>>> fuzzy_or([None, None])

which then also gives None.

Relations between different symbols#

A fundamental limitation of the old assumptions system is that all explicit assumptions are properties of an individual symbol. There is no way in this system to make an assumption about the relationship between two symbols. One of the most common requests is the ability to assume something like x < y but there is no way to even specify that in the old assumptions.

The new assumptions have the theoretical capability that relational assumptions can be specified. However the algorithms to make use of that information are not yet implemented and the exact API for specifying relational assumptions has not been decided upon.