What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
A truth table generator is a small interpreter. It reads a logic formula as text, turns it into a tree, evaluates that tree once for every combination of truth values assigned to its variables, and reads the answers off the results. The tautology question comes from the same data: a formula is a tautology when every row is true. This article builds that pipeline in standard-library Python with a bounded grammar, and it deliberately avoids eval().
Why not use eval()?
The shortcut is to pass the formula to eval() with a dictionary of variable values. It works for a few simple cases, but it fails in two ways that matter here.
As an Amazon Associate I earn from qualifying purchases.
First, eval() executes arbitrary Python. A formula string becomes code with access to whatever the interpreter can reach. Second, Python’s operators do not behave like logical connectives. Python’s ~ is bitwise negation, so ~True evaluates to -2, not False. Python’s and and or return one of their operands, which is only safe when every operand is a real boolean. Accepting Python syntax by accident means inheriting those rules. A custom grammar lets you define the semantics yourself.
The input language
The interpreter accepts four operators, two constants, and variables. Precedence decides how A | B & C groups, so it has to be fixed before the parser is written.
#1 Best Overall
| Symbol | Meaning | Precedence | Associativity |
|---|---|---|---|
~ |
not | Highest (binds tightest) | Prefix; ~~A is allowed |
& |
and | Second | Left |
| |
or | Third | Left |
-> |
implies (false only when the left side is true and the right side is false) | Fourth | Right: A -> B -> C means A -> (B -> C) |
<-> |
if and only if (true when both sides have the same value) | Lowest | Left |
- Constants are
0(false) and1(true). - Variables start with a letter and may contain letters, digits, and underscores, for example
p,Rain_1. Names are case-sensitive, soAandaare different variables. - Spaces and tabs are ignored. Any other character is an error.
- Only the words and symbols above exist. There are no quantifiers, predicates, or user-defined functions.
Step 1: Tokenize the text
The tokenizer converts the string into a list of tokens. Each token records its kind, its text, and its position, so later error messages can point at the exact character. Anything it cannot classify raises an error immediately.
import re
from dataclasses import dataclass
from itertools import product
class LogicError(Exception):
'''Raised for any malformed formula.'''
# ---------- 1. Tokenizer ----------
TOKEN_SPEC = [
('SKIP', r'\s+'),
('IFF', r'<->'),
('IMP', r'->'),
('NOT', r'~'),
('AND', r'&'),
('OR', r'\|'),
('LPAREN', r'\('),
('RPAREN', r'\)'),
('CONST', r'[01]'),
('VAR', r'[A-Za-z][A-Za-z0-9_]*'),
('BAD', r'\S'),
]
MASTER = re.compile('|'.join('(?P<%s>%s)' % (name, pat) for name, pat in TOKEN_SPEC))
@dataclass(frozen=True)
class Token:
kind: str
text: str
pos: int
def tokenize(source):
tokens = []
for m in MASTER.finditer(source):
kind, text = m.lastgroup, m.group()
if kind == 'SKIP':
continue
if kind == 'BAD':
raise LogicError(f'unexpected character {text!r} at position {m.start()}')
tokens.append(Token(kind, text, m.start()))
tokens.append(Token('EOF', '', len(source)))
return tokens
Order in TOKEN_SPEC matters. <-> is tried before ->, and the catch-all BAD pattern comes last. A lone - therefore becomes an error rather than being silently skipped.
Step 2: Parse the tokens into a tree
The parser is recursive descent with one function per precedence level. The grammar, from lowest precedence to highest, is:
- formula → iff
- iff → imp ( ‘<->’ imp )*
- imp → disj [ ‘->’ imp ]
- disj → conj ( ‘|’ conj )*
- conj → unary ( ‘&’ unary )*
- unary → ‘~’ unary | atom
- atom → VARIABLE | CONSTANT | ‘(‘ iff ‘)’
Each level calls the next tighter level, so precedence is encoded in the call structure itself. The implication rule recurses on its right side, which makes -> right-associative. The parser produces four node types, and the tree never contains text or Python operators.
Rank #2
# ---------- 2. Parser (recursive descent) ----------
@dataclass(frozen=True)
class Var:
name: str
@dataclass(frozen=True)
class Const:
value: bool
@dataclass(frozen=True)
class Not:
operand: object
@dataclass(frozen=True)
class Binary:
op: str # '&', '|', '->' or '<->'
left: object
right: object
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.i = 0
def peek(self):
return self.tokens[self.i]
def advance(self):
tok = self.tokens[self.i]
self.i += 1
return tok
def fail(self, expected):
tok = self.peek()
if tok.kind == 'EOF':
where = 'end of input'
else:
where = repr(tok.text) + ' at position ' + str(tok.pos)
raise LogicError('expected ' + expected + ', found ' + where)
def parse(self):
node = self.iff()
if self.peek().kind != 'EOF':
self.fail('an operator or end of input')
return node
def iff(self): # <-> lowest, left-associative
node = self.imp()
while self.peek().kind == 'IFF':
self.advance()
node = Binary('<->', node, self.imp())
return node
def imp(self): # -> right-associative
left = self.disj()
if self.peek().kind == 'IMP':
self.advance()
return Binary('->', left, self.imp())
return left
def disj(self): # | left-associative
node = self.conj()
while self.peek().kind == 'OR':
self.advance()
node = Binary('|', node, self.conj())
return node
def conj(self): # & left-associative
node = self.unary()
while self.peek().kind == 'AND':
self.advance()
node = Binary('&', node, self.unary())
return node
def unary(self): # ~ prefix, binds tightest
if self.peek().kind == 'NOT':
self.advance()
return Not(self.unary())
return self.atom()
def atom(self):
tok = self.peek()
if tok.kind == 'VAR':
self.advance()
return Var(tok.text)
if tok.kind == 'CONST':
self.advance()
return Const(tok.text == '1')
if tok.kind == 'LPAREN':
self.advance()
node = self.iff()
if self.peek().kind != 'RPAREN':
self.fail('a closing parenthesis for the opening one')
self.advance()
return node
self.fail('a variable, a constant, ~, or an opening parenthesis')
def parse(source):
return Parser(tokenize(source)).parse()
Parsing A | B & C yields Binary('|', Var('A'), Binary('&', Var('B'), Var('C'))): the conjunction is grouped first, as the table requires.
Step 3: Evaluate the tree under one assignment
The evaluator walks the tree recursively. It receives an environment that maps each variable name to a Python bool and returns a bool. Because the values come from this program’s own enumeration and never from user-supplied Python, the operators and and or are safe to use here. The grammar’s operators are translated into explicit truth functions in one place.
# ---------- 3. Evaluator ----------
def evaluate(node, env):
if isinstance(node, Const):
return node.value
if isinstance(node, Var):
return env[node.name]
if isinstance(node, Not):
return not evaluate(node.operand, env)
a = evaluate(node.left, env)
b = evaluate(node.right, env)
if node.op == '&':
return a and b
if node.op == '|':
return a or b
if node.op == '->':
return (not a) or b
return a == b # '<->'
def variables(node):
if isinstance(node, Var):
return {node.name}
if isinstance(node, Const):
return set()
if isinstance(node, Not):
return variables(node.operand)
return variables(node.left) | variables(node.right)
Step 4: Build the table and classify the formula
Variables are sorted by name, so column order is deterministic. Because sorted compares character codes, uppercase names come before lowercase ones. itertools.product then yields every assignment in the conventional order, starting with all variables false. A formula with no variables, such as 1, produces exactly one row.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteClassification uses only the result column. A formula is a tautology when every result is true, a contradiction when none is, and satisfiable when at least one is.
# ---------- 4. Truth table and classification ----------
def symbol(flag):
return 'T' if flag else 'F'
def truth_table(source):
formula = parse(source)
names = sorted(variables(formula))
rows = []
for values in product([False, True], repeat=len(names)):
env = dict(zip(names, values))
rows.append((values, evaluate(formula, env)))
return names, rows
def classify(source):
_, rows = truth_table(source)
results = [result for _, result in rows]
return {
'tautology': all(results),
'contradiction': not any(results),
'satisfiable': any(results),
}
def print_table(source):
names, rows = truth_table(source)
header = ' '.join(names) + ' | result'
print(header)
print('-' * len(header))
for values, result in rows:
print(' '.join(symbol(v) for v in values) + ' | ' + symbol(result))
if __name__ == '__main__':
import sys
print_table(sys.argv[1])
Running it
Save the four sections as truthtable.py and run it from a terminal, quoting the formula so the shell does not interpret &, |, or <:
python truthtable.py "A -> B"
A B | result
------------
F F | T
F T | T
T F | F
T T | T
python truthtable.py "(A -> B) <-> (~B -> ~A)"
A B | result
------------
F F | T
F T | T
T F | T
T T | T
The second formula is the contrapositive equivalence. Every row is true, so classify reports a tautology. The first formula is satisfiable but not a tautology, because the row where A is true and B is false is false.
What malformed input produces
Each error reports the point where the parser could not continue. For a missing operand, that is the end of input rather than the operator that needed the operand.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →| Input | Message raised |
|---|---|
| empty string | expected a variable, a constant, ~, or an opening parenthesis, found end of input |
A & |
expected a variable, a constant, ~, or an opening parenthesis, found end of input |
(A | B |
expected a closing parenthesis for the opening one, found end of input |
A B |
expected an operator or end of input, found ‘B’ at position 2 |
A & B) |
expected an operator or end of input, found ‘)’ at position 5 |
A - B |
unexpected character ‘-‘ at position 2 |
How large the table gets
Each variable doubles the number of rows, because each one has two truth values. The enumeration is exhaustive, so the number of evaluations is 2n for n variables, and each evaluation walks the whole tree. These counts follow from that arithmetic alone; they are not measured run times.
| Variables (n) | Rows (2n) |
|---|---|
| 1 | 2 |
| 2 | 4 |
| 3 | 8 |
| 5 | 32 |
| 10 | 1,024 |
| 20 | 1,048,576 |
| 30 | 1,073,741,824 |
Tests worth writing
The following pytest file covers constants, negation, precedence, parentheses, the three classification outcomes, and malformed input. Save it beside truthtable.py and run pytest.
import pytest
from truthtable import LogicError, Binary, Var, Not, classify, parse
def test_constants_and_single_variable():
assert classify('1') == {'tautology': True, 'contradiction': False, 'satisfiable': True}
assert classify('0')['satisfiable'] is False
assert classify('A')['tautology'] is False
def test_negation_and_precedence():
assert parse('~A & B') == Binary('&', Not(Var('A')), Var('B'))
assert parse('A | B & C') == Binary('|', Var('A'), Binary('&', Var('B'), Var('C')))
assert parse('A -> B -> C') == Binary('->', Var('A'), Binary('->', Var('B'), Var('C')))
def test_parentheses_override_precedence():
assert parse('(A | B) & C') == Binary('&', Binary('|', Var('A'), Var('B')), Var('C'))
def test_tautology_contradiction_and_mixed():
assert classify('A | ~A')['tautology'] is True
assert classify('A & ~A')['contradiction'] is True
mixed = classify('A -> B')
assert mixed['satisfiable'] is True and mixed['tautology'] is False
def test_malformed_input_raises():
for bad in ['', 'A &', '(A | B', 'A B', 'A - B', 'A & B)']:
with pytest.raises(LogicError):
parse(bad)
Where this design stops
Row count grows too fast for large formulas
Printing or checking every row is practical for a handful of variables and becomes impractical as the count grows. For a tautology test, a satisfiability solver is the better tool: a formula is a tautology exactly when its negation has no satisfying assignment. SymPy’s satisfiable function returns a model when one exists and False when none does, so the check is to negate the formula and ask whether the result is satisfiable.
Deep nesting exhausts the call stack
Each level of parentheses costs about six nested function calls here (iff, imp, disj, conj, unary, atom). In CPython, whose default recursion limit is 1,000 frames, input nested around 160 levels deep raises RecursionError. Catch that exception at the call site and report the formula as too deeply nested, or reject inputs above a length you choose.
Recommended Free Tools
Adding an operator touches three places
A new connective needs a token pattern in TOKEN_SPEC, a rule at the correct precedence level in the parser, and a branch in evaluate. Keeping the three stages separate is what makes each one testable on its own.
Best Value
Existing libraries and the Python caveat
SymPy’s logic module documents Boolean expression construction, truth-table iteration, satisfiability, and conversion to conjunctive and disjunctive normal forms. Its Boolean expressions are built with the overloaded operators &, |, and ~, and it also supports implication operators.
Do not use Python’s native if, and, or, or not on symbolic expressions. SymPy’s guide to symbolic Booleans explains that such use can raise an error, because Python needs a definite True or False. Use And, Or, and Not, or the overloaded operators. Python’s official expression reference is the authority for how Python’s own operators work, and it is a different language from the grammar built here.
SymPy’s parsing documentation covers several input mechanisms, but its LaTeX parser is documented as experimental and subject to change. Do not treat it as a hardened parser for untrusted input, and do not rely on it when you need the strict, predictable grammar described above.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Two other packages target the same problem. ttable is listed on PyPI as a toolkit for Boolean expressions and truth tables. Mathematical Logic through Python is a teaching API that prints truth tables and describes tautology and satisfiability semantics. This article does not verify the current release status, maintenance, or API quality of either package, so check each project’s release history before depending on one.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




