Tech With Galvan
Back to Articles
Development5/26/2025 • 4 min read

Text Difference Checker using Python and Streamlit

Compare two texts line by line with Python and Streamlit — added, removed, and changed lines highlighted with difflib and stats.

Galvan
Galvan

Founder & Creator

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 — SequenceMatcher ratio as a percentage.
  • Change stats — counts of added, removed, and modified lines.

Prerequisites

  • Python 3.8+ — difflib is 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=True missing; 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 SequenceMatcher on 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”.