# Symbolic and fuzzy booleans#

This page describes what a symbolic `Boolean` in SymPy is and also how that relates to three-valued fuzzy-bools that are used in many parts of SymPy. It also discusses some common problems that arise when writing code that uses three-valued logic and how to handle them correctly.

## Symbolic Boolean vs three valued bool#

Assumptions queries like `x.ispositive` give fuzzy-bool `True`, `False` or `None` results 1. These are low-level Python objects rather than SymPy’s symbolic `Boolean` expressions.

```>>> from sympy import Symbol, symbols
>>> xpos = Symbol('xpos', positive=True)
>>> xneg = Symbol('xneg', negative=True)
>>> x = Symbol('x')
>>> print(xpos.is_positive)
True
>>> print(xneg.is_positive)
False
>>> print(x.is_positive)
None
```

A `None` result as a fuzzy-bool should be interpreted as meaning “maybe” or “unknown”.

An example of a symbolic `Boolean` class in SymPy can be found when using inequalities. When an inequality is not known to be true or false a `Boolean` can represent indeterminate results symbolically:

```>>> xpos > 0
True
>>> xneg > 0
False
>>> x > 0
x > 0
>>> type(x > 0)
<class 'sympy.core.relational.StrictGreaterThan'>
```

The last example shows what happens when an inequality is indeterminate: we get an instance of `StrictGreaterThan` which represents the inequality as a symbolic expression. Internally when attempting to evaluate an inequality like `a > b` SymPy will compute `(a - b).is_extended_positive`. If the result is `True` or `False` then SymPy’s symbolic `S.true` or `S.false` will be returned. If the result is `None` then an unevaluated `StrictGreaterThan` is returned as shown for `x > 0` above.

It is not obvious that queries like `xpos > 0` return `S.true` rather than `True` because both objects display in the same way but we can check this using the Python `is` operator:

```>>> from sympy import S
>>> xpos.is_positive is True
True
>>> xpos.is_positive is S.true
False
>>> (xpos > 0) is True
False
>>> (xpos > 0) is S.true
True
```

There is no general symbolic analogue of `None` in SymPy. In the cases where a low-level assumptions query gives `None` the symbolic query will result in an unevaluated symbolic `Boolean` (e.g, `x > 0`). We can use a symbolic `Boolean` as part of a symbolic expression such as a `Piecewise`:

```>>> from sympy import Piecewise
>>> p = Piecewise((1, x > 0), (2, True))
>>> p
Piecewise((1, x > 0), (2, True))
>>> p.subs(x, 3)
1
```

Here `p` represents an expression that will be equal to `1` if `x > 0` or otherwise it will be equal to `2`. The unevaluated `Boolean` inequality `x > 0` represents the condition for deciding the value of the expression symbolically. When we substitute a value for `x` the inequality will resolve to `S.true` and then the `Piecewise` can evaluate to `1` or `2`.

The same will not work when using a fuzzy-bool instead of a symbolic `Boolean`:

```>>> p2 = Piecewise((1, x.is_positive), (2, True))
Traceback (most recent call last):
...
TypeError: Second argument must be a Boolean, not `NoneType`
```

The `Piecewise` can not use `None` as the condition because unlike the inequality `x > 0` it gives no information. With the inequality it is possible to decide in future if the condition might `True` or `False` once a value for `x` is known. A value of `None` can not be used in that way so it is rejected.

Note

We can use `True` in the `Piecewise` because `True` sympifies to `S.true`. Sympifying `None` just gives `None` again which is not a valid symbolic SymPy object.

There are many other symbolic `Boolean` types in SymPy. The same considerations about the differences between fuzzy bool and symbolic `Boolean` apply to all other SymPy `Boolean` types. To give a different example there is `Contains` which represents the statement that an object is contained in a set:

```>>> from sympy import Reals, Contains
>>> x = Symbol('x', real=True)
>>> y = Symbol('y')
>>> Contains(x, Reals)
True
>>> Contains(y, Reals)
Contains(y, Reals)
>>> Contains(y, Reals).subs(y, 1)
True
```

The Python operator corresponding to `Contains` is `in`. A quirk of `in` is that it can only evaluate to a `bool` (`True` or `False`) so if the result is indeterminate then an exception will be raised:

```>>> from sympy import I
>>> 2 in Reals
True
>>> I in Reals
False
>>> x in Reals
True
>>> y in Reals
Traceback (most recent call last):
...
TypeError: did not evaluate to a bool: (-oo < y) & (y < oo)
```

The exception can be avoided by using `Contains(x, Reals)` or `Reals.contains(x)` rather than `x in Reals`.

## Three-valued logic with fuzzy bools#

Whether we use the fuzzy-bool or symbolic `Boolean` we always need to be aware of the possibility that a query might be indeterminate. How to write code that handles this is different in the two cases though. We will look at fuzzy-bools first.

Consider the following function:

```>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive and b.is_positive:
...         return True
...     else:
...         return False
```

The `both_positive` function is supposed to tell us whether or not `a` and `b` are both positive. However the `both_positive` function will fail if either of the `is_positive` queries gives `None`:

```>>> print(both_positive(S(1), S(1)))
True
>>> print(both_positive(S(1), S(-1)))
False
>>> print(both_positive(S(-1), S(-1)))
False
>>> x = Symbol('x') # may or may not be positive
>>> print(both_positive(S(1), x))
False
```

Note

We need to sympify the arguments to this function using `S` because the assumptions are only defined on SymPy objects and not regular Python `int` objects.

Here `False` is incorrect because it is possible that `x` is positive in which case both arguments would be positive. We get `False` here because `x.is_positive` gives `None` and Python will treat `None` as “falsey”.

In order to handle all possible cases correctly we need to separate the logic for identifying the `True` and `False` cases. An improved function might be:

```>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive is False or b.is_positive is False:
...         return False
...     elif a.is_positive is True and b.is_positive is True:
...         return True
...     else:
...         return None
```

This function now can handle all cases of `True`, `False` or `None` for both `a` and `b` and will always return a fuzzy bool representing whether the statement “`a` and `b` are both positive” is true, false or unknown:

```>>> print(both_positive_better(S(1), S(1)))
True
>>> print(both_positive_better(S(1), S(-1)))
False
>>> x = Symbol('x')
>>> y = Symbol('y', positive=True)
>>> print(both_positive_better(S(1), x))
None
>>> print(both_positive_better(S(-1), x))
False
>>> print(both_positive_better(S(1), y))
True
```

Another case that we need to be careful of when using fuzzy-bools is negation with Python’s `not` operator e.g.:

```>>> x = Symbol('x')
>>> print(x.is_positive)
None
>>> not x.is_positive
True
```

The correct negation of a fuzzy bool `None` is `None` again. If we do not know whether the statement “`x` is positive” is `True` or `False` then we also do not know whether its negation “`x` is not positive” is `True` or `False`. The reason we get `True` instead is again because `None` is considered “falsey”. When `None` is used with a logical operator such as `not` it will first be converted to a `bool` and then negated:

```>>> bool(None)
False
>>> not bool(None)
True
>>> not None
True
```

The fact that `None` is treated as falsey can be useful if used correctly. For example we may want to do something only if `x` is known to positive in which case we can do

```>>> x = Symbol('x', positive=True)
>>> if x.is_positive:
...     print("x is definitely positive")
... else:
...     print("x may or may not be positive")
x is definitely positive
```

Provided we understand that an alternate condition branch refers to two cases (`False` and `None`) then this can be a useful way of writing conditionals. When we really do need to distinguish all cases then we need to use things like `x.is_positive is False`. What we need to be careful of though is using Python’s binary logic operators like `not` or `and` with fuzzy bools as they will not handle the indeterminate case correctly.

In fact SymPy has internal functions that are designed to handle fuzzy-bools correctly:

```>>> from sympy.core.logic import fuzzy_not, fuzzy_and
>>> print(fuzzy_not(True))
False
>>> print(fuzzy_not(False))
True
>>> print(fuzzy_not(None))
None
>>> print(fuzzy_and([True, True]))
True
>>> print(fuzzy_and([True, None]))
None
>>> print(fuzzy_and([False, None]))
False
```

Using the `fuzzy_and` function we can write the `both_positive` function more simply:

```>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return fuzzy_and([a.is_positive, b.is_positive])
```

Making use of `fuzzy_and`, `fuzzy_or` and `fuzzy_not` leads to simpler code and can also reduce the chance of introducing a logic error because the code can look more like it would in the case of ordinary binary logic.

## Three-valued logic with symbolic Booleans#

When working with symbolic `Boolean` rather than fuzzy-bool the issue of `None` silently being treated as falsey does not arise so it is easier not to end up with a logic error. However instead the indeterminate case will often lead to an exception being raised if not handled carefully.

We will try to implement the `both_positive` function this time using symbolic `Boolean`:

```>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a > 0 and b > 0:
...         return S.true
...     else:
...         return S.false
```

The first difference is that we return the symbolic `Boolean` objects `S.true` and `S.false` rather than `True` and `False`. The second difference is that we test e.g. `a > 0` rather than `a.is_positive`. Trying this out we get

```>>> both_positive(1, 2)
True
>>> both_positive(-1, 1)
False
>>> x = Symbol('x')  # may or may not be positive
>>> both_positive(x, 1)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
```

What happens now is that testing `x > 0` gives an exception when `x` is not known to be positive or not positive. More precisely `x > 0` does not give an exception but `if x > 0` does and that is because the `if` statement implicitly calls `bool(x > 0)` which raises.

```>>> x > 0
x > 0
>>> bool(x > 0)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
>>> if x > 0:
...     print("x is positive")
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
```

The Python expression `x > 0` creates a SymPy `Boolean`. Since in this case the `Boolean` can not evaluate to `True` or `False` we get an unevaluated `StrictGreaterThan`. Attempting to force that into a `bool` with `bool(x > 0)` raises an exception. That is because a regular Python `bool` must be either `True` or `False` and neither of those are known to be correct in this case.

The same kind of issue arises when using `and`, `or` or `not` with symbolic `Boolean`. The solution is to use SymPy’s symbolic `And`, `Or` and `Not` or equivalently Python’s bitwise logical operators `&`, `|` and `~`:

```>>> from sympy import And, Or, Not
>>> x > 0
x > 0
>>> x > 0 and x < 1
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
>>> And(x > 0, x < 1)
(x > 0) & (x < 1)
>>> (x > 0) & (x < 1)
(x > 0) & (x < 1)
>>> Or(x < 0, x > 1)
(x > 1) | (x < 0)
>>> Not(x < 0)
x >= 0
>>> ~(x < 0)
x >= 0
```

As before we can make a better version of `both_positive` if we avoid directly using a SymPy `Boolean` in an `if`, `and`, `or`, or `not`. Instead we can test whether or not the `Boolean` has evaluated to `S.true` or `S.false`:

```>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if (a > 0) is S.false or (b > 0) is S.false:
...         return S.false
...     elif (a > 0) is S.true and (b > 0) is S.true:
...         return S.true
...     else:
...         return And(a > 0, b > 0)
```

Now with this version we don’t get any exceptions and if the result is indeterminate we will get a symbolic `Boolean` representing the conditions under which the statement “`a` and `b` are both positive” would be true:

```>>> both_positive_better(S(1), S(2))
True
>>> both_positive_better(S(1), S(-1))
False
>>> x, y = symbols("x, y")
>>> both_positive_better(x, y + 1)
(x > 0) & (y + 1 > 0)
>>> both_positive_better(x, S(3))
x > 0
```

The last case shows that actually using the `And` with a condition that is known to be true simplifies the `And`. In fact we have

```>>> And(x > 0, 3 > 0)
x > 0
>>> And(4 > 0, 3 > 0)
True
>>> And(-1 > 0, 3 > 0)
False
```

What this means is that we can improve `both_positive_better`. The different cases are not needed at all. Instead we can simply return the `And` and let it simplify if possible:

```>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return And(a > 0, b > 0)
```

Now this will work with any symbolic real objects and produce a symbolic result. We can also substitute into the result to see how it would work for particular values:

```>>> both_positive_best(2, 1)
True
>>> both_positive_best(-1, 2)
False
>>> both_positive_best(x, 3)
x > 0
>>> condition = both_positive_best(x/y, x + y)
>>> condition
(x + y > 0) & (x/y > 0)
>>> condition.subs(x, 1)
(1/y > 0) & (y + 1 > 0)
>>> condition.subs(x, 1).subs(y, 2)
True
```

The idea when working with symbolic `Boolean` objects is as much as possible to avoid trying to branch on them with `if/else` and other logical operators like `and` etc. Instead think of computing a condition and passing it around as a variable. The elementary symbolic operations like `And`, `Or` and `Not` can then take care of the logic for you.

Footnotes

1

Note that what is referred to in SymPy as a “fuzzy bool” is really about using three-valued logic. In normal usage “fuzzy logic” refers to a system where logical values are continuous in between zero and one which is something different from three-valued logic.