Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk10 min

Build a Truth Table Generator in Python: Parser, Evaluator, and Tautology Checker

Build a small propositional-logic interpreter in standard-library Python. It tokenizes formulas, parses them with fixed precedence, evaluates each row safely, and detects tautologies, contradictions, and satisfiable formulas.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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) and 1 (true).
  • Variables start with a letter and may contain letters, digits, and underscores, for example p, Rain_1. Names are case-sensitive, so A and a are 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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

# ---------- 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Classification 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Two 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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.