Boolean logic shows up everywhere: if conditions, access-control rules, query filters, circuit design, SAT solvers. Truth tables are the simplest way to check that an expression does what you think it does.
This post covers the core idea, the one operator that causes most mistakes, and a small Python script you can reuse.
What a truth table is
A truth table lists every possible True/False assignment for your variables and the result of your expression for each one. With n variables you get 2^n rows, so 3 variables is 8 rows and 8 variables is 256. That growth is why you stop doing this by hand fairly quickly.
The operators
| Operator | Meaning | False when |
|---|---|---|
A AND B |
both must be true | either is false |
A OR B |
at least one true (inclusive) | both false |
NOT A |
inverts | A is true |
A XOR B |
exactly one true | both the same |
A -> B |
material conditional | A true and B false |
A <-> B |
biconditional | values differ |
The one that trips everyone: implication
A -> B is false in exactly one case: A is true and B is false. If A is false, the whole thing is true regardless of B (vacuous truth).
| A | B | A → B |
|---|---|---|
| T | T | T |
| T | F | F |
| F | T | T |
| F | F | T |
This is not a quirk. It's what makes the contrapositive valid: A -> B is equivalent to NOT B -> NOT A. It's also why A -> B is equivalent to (NOT A) OR B, which is how you'd write it in most programming languages.
A truth table generator in Python
from itertools import product
def truth_table(expr, variables):
print(*variables, "|", expr)
for values in product([True, False], repeat=len(variables)):
env = dict(zip(variables, values))
result = eval(expr, {}, env) # fine for your own input, never for untrusted strings
print(*[int(v) for v in values], "|", int(result))
truth_table("(not a) or b", ["a", "b"])
Output:
a b | (not a) or b
1 1 | 1
1 0 | 0
0 1 | 1
0 0 | 1
That's the implication table from above. Python has no implication operator, so (not a) or b stands in for it.
Checking logical equivalence
Two expressions are logically equivalent if they produce the same output for every assignment:
def equivalent(expr1, expr2, variables):
for values in product([True, False], repeat=len(variables)):
env = dict(zip(variables, values))
if eval(expr1, {}, env) != eval(expr2, {}, env):
return False
return True
# De Morgan's law
print(equivalent("not (a and b)", "(not a) or (not b)", ["a", "b"])) # True
# Common mistake: not (a and b) is NOT the same as (not a) and (not b)
print(equivalent("not (a and b)", "(not a) and (not b)", ["a", "b"])) # False
The second check is a good test of whether you've internalized De Morgan's laws. Refactoring a condition by "distributing the not" and getting it wrong is a classic source of bugs.
Identities worth knowing
-
De Morgan:
NOT (P AND Q) ≡ (NOT P) OR (NOT Q), andNOT (P OR Q) ≡ (NOT P) AND (NOT Q) -
Material implication:
P -> Q ≡ (NOT P) OR Q -
Contrapositive:
P -> Q ≡ NOT Q -> NOT P -
Double negation:
NOT NOT P ≡ P -
Absorption:
P OR (P AND Q) ≡ P
Classifying expressions
Once you can enumerate every row, you can classify any expression:
-
Tautology: true for every assignment (
P OR NOT P) -
Contradiction: false for every assignment (
P AND NOT P) - Contingency: true for some, false for others
Spotting a tautology or contradiction inside a real condition usually means dead code or a logic bug.
If you'd rather not write the script
I built a free browser-based generator that does this with a proper parser instead of eval. It supports up to 8 variables, shows intermediate sub-expression columns, flags tautologies and contradictions, has an equivalence checker, and exports CSV. It runs entirely client-side, so nothing is sent to a server:
Disclosure: this tool is hosted on my own site.
If you spot a precedence or parsing edge case, let me know in the comments. Operator precedence in the tool is NOT > AND > XOR > OR > IMPLIES > EQUIV.
Top comments (0)