Introduction
“What changed between these two versions?” — a question you usually answer by squinting at two documents side by side. This app answers it programmatically: paste an original and a revised text, and get every added, removed, and changed line highlighted, plus similarity stats. It is built entirely on difflib, Python’s underrated built-in diff engine — the same module behind git diff-style output.
If the Markdown editor showed you two panes of one document, this shows you one pane of two documents.
Features
- Two-pane input — original and revised text areas.
- Line-level diff — added lines in green, removed in red, changed pairs side by side.
- Unified diff view — classic
--- / +++format for the git-familiar. - Similarity score —
SequenceMatcherratio as a percentage. - Change stats — counts of added, removed, and modified lines.
Prerequisites
- Python 3.8+ —
difflibis built in. - Streamlit:
pip install streamlit
Step 1: Create the Script
Save as diff_checker.py:
import streamlit as st
import difflib
st.set_page_config(page_title="Text Difference Checker", page_icon="🔀")
st.title("🔀 Text Difference Checker")
col1, col2 = st.columns(2)
original = col1.text_area("Original", height=250, placeholder="Paste the original text...")
revised = col2.text_area("Revised", height=250, placeholder="Paste the revised text...")
if original and revised:
orig_lines, rev_lines = original.splitlines(), revised.splitlines()
ratio = difflib.SequenceMatcher(None, orig_lines, rev_lines).ratio()
added = removed = changed = 0
sm = difflib.SequenceMatcher(None, orig_lines, rev_lines)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag == "delete":
removed += i2 - i1
elif tag == "insert":
added += j2 - j1
elif tag == "replace":
changed += max(i2 - i1, j2 - j1)
c1, c2, c3, c4 = st.columns(4)
c1.metric("Similarity", f"{ratio:.0%}")
c2.metric("Added", added)
c3.metric("Removed", removed)
c4.metric("Changed", changed)
tab_side, tab_unified = st.tabs(["Side by side", "Unified diff"])
with tab_side:
html = difflib.HtmlDiff().make_table(orig_lines, rev_lines,
"Original", "Revised",
context=True, numlines=2)
st.markdown(
f"<div style='zoom:0.85'>{html}</div>",
unsafe_allow_html=True,
)
with tab_unified:
diff = difflib.unified_diff(orig_lines, rev_lines,
fromfile="original", tofile="revised", lineterm="")
st.code("\n".join(diff), language="diff")
else:
st.info("Paste text into both boxes to compare.")
Step 2: Run the App
streamlit run diff_checker.py
Paste a paragraph into Original, edit it, paste the edit into Revised — the metrics and diff views populate instantly.
How It Works
SequenceMatcher is the brain: it finds the longest matching blocks between two sequences and expresses everything else as opcodes — equal, replace, delete, insert spans. Iterating opcodes gives you both the human story (which lines changed) and the stats (how much). Feeding it lines (from splitlines()) gives line-level diffs; feeding it characters would give character-level ones — same class, different granularity.
The two views come from two helpers built on that same engine. HtmlDiff.make_table produces a complete, self-contained HTML table with green/red row highlighting, which the app embeds via st.markdown(..., unsafe_allow_html=True) — the established pattern from the color palette generator. unified_diff emits the ---/+++ text format every developer recognizes from git, rendered in a code block with diff highlighting.
The similarity ratio is 2 * matches / (len_a + len_b) — a quick, defensible “how alike are these” number.
Common Errors & Fixes
- HTML table shows as raw code —
unsafe_allow_html=Truemissing; difflib’s output is a full HTML table, not Markdown. - Diff looks wrong on reordered paragraphs — SequenceMatcher matches blocks, so moved text shows as delete+insert; that’s correct behavior, just not a move-detector.
- Huge tables freeze the browser — thousands of lines produce a giant HTML table; add
context=True(already on) or diff in chunks. - Trailing-newline lines vanish —
splitlines()drops the final empty string; append a sentinel if exact blank-line accounting matters.
Key Concepts
- Opcodes — the vocabulary of every diff: equal/replace/delete/insert spans.
- Granularity choice — lines vs characters changes the story a diff tells.
- Two renderers, one engine — HtmlDiff and unified_diff share SequenceMatcher.
- Similarity ratio — matches doubled, divided by total length.
What to Try Next
- Add word-level mode — split on spaces instead of lines for prose editing.
- Highlight changed characters within a replaced line using
SequenceMatcheron the pair. - Accept file uploads instead of paste — pair with the PDF merger for document workflows.
- Add an ignore whitespace/case toggle that normalizes inputs before diffing.
FAQ
How is this different from git diff?
Same core algorithm family. Git adds optimizations (Myers’ algorithm, rename detection) for huge repositories; difflib is pure Python, dependency-free, and perfect for documents this size.
Can it compare Word documents?
Not directly — extract text first (e.g. python-docx), then feed the lines here. The app only cares about strings.
Why does similarity say 92% but the diff looks big?
The ratio weighs matching characters across the whole text — a few edits in a long document barely move it. Trust the opcode counts for “how much changed” and the ratio for “how much survived”.