Python difflib: Compare Text and Files

Published on: August 1, 2026
Reading time: 6 minutes
Text documents representing version comparison with Python difflib

Comparing two versions of text is useful in reviews, audits, migrations, tests, synchronization, and command-line tools. Instead of only knowing that content differs, applications often need to show which lines were removed, added, or changed. The Python difflib module provides sequence matching, human-readable deltas, similarity suggestions, and HTML reports without external dependencies.

This guide covers SequenceMatcher, get_close_matches(), unified_diff(), context_diff(), ndiff(), restore(), and HtmlDiff. It complements our articles about Python lists, slicing, temporary files, collections, and reading text files.

What difflib compares

difflib works with sequences whose elements are hashable. This includes strings, lists of lines, and token sequences.

from difflib import SequenceMatcher

old = "Python is simple"
new = "Python is very simple"

matcher = SequenceMatcher(None, old, new)
print(matcher.ratio())

The algorithm finds long contiguous matching blocks and applies the same idea recursively to the remaining portions. It favors matches that look natural to people, but it does not promise a minimal edit script.

Similarity with ratio()

ratio() returns a value between zero and one. Values close to one indicate strong similarity.

similarity = SequenceMatcher(None, "configuration", "configuraton").ratio()
print(round(similarity, 3))

The score is not a probability and has no universal threshold. Calibrate a cutoff with real examples and measure false positives and false negatives.

Argument order can matter

The official difflib documentation warns that ratio() can change when the sequences are reversed.

print(SequenceMatcher(None, "tide", "diet").ratio())
print(SequenceMatcher(None, "diet", "tide").ratio())

If a business rule needs a symmetric score, compute both directions or choose a metric designed to be symmetric.

quick_ratio() and real_quick_ratio()

These methods return faster upper bounds for the complete ratio.

matcher = SequenceMatcher(None, "abcd", "bcde")
print(matcher.real_quick_ratio())
print(matcher.quick_ratio())
print(matcher.ratio())

In a large candidate search, use the estimates to reject obvious mismatches and calculate the full score only for survivors.

Finding matching blocks

get_matching_blocks() reports positions and sizes of equal blocks.

matcher = SequenceMatcher(None, "abxcd", "abcd")
for block in matcher.get_matching_blocks():
    print(block)

The final block is always a zero-size sentinel. This API is useful for custom highlighting and visualizations.

Transformations with get_opcodes()

get_opcodes() describes how to transform the first sequence into the second.

a = "qabxcd"
b = "abycdf"
matcher = SequenceMatcher(None, a, b)

for tag, i1, i2, j1, j2 in matcher.get_opcodes():
    print(tag, a[i1:i2], b[j1:j2])

Tags are equal, replace, delete, and insert. Structured opcodes are easier to consume in a UI than formatted diff text.

Comparing many inputs against one reference

SequenceMatcher caches details about the second sequence. Set a shared reference once when many inputs are compared with the same catalog value.

matcher = SequenceMatcher(None)
matcher.set_seq2("configuration")

for entry in ["configuraton", "config", "configurations"]:
    matcher.set_seq1(entry)
    print(entry, matcher.ratio())

This can reduce repeated work in typo correction and record normalization.

Suggestions with get_close_matches()

get_close_matches() returns the best candidates above a cutoff.

from difflib import get_close_matches

commands = ["install", "inspect", "initialize", "interrupt"]
print(get_close_matches("instal", commands, n=3, cutoff=0.6))

It is useful for CLIs, field validation, and “did you mean?” messages. It is not semantic search because it compares sequence form rather than meaning.

The autojunk heuristic

For sequences with at least 200 elements, the automatic heuristic treats highly repeated items as popular and reduces their role in matching.

matcher = SequenceMatcher(None, seq_a, seq_b, autojunk=False)

Disable autojunk when repetition is meaningful, such as DNA, structured logs, or code lists. Benchmark the change because disabling it can increase cost.

Custom junk elements

The first constructor argument can mark elements that should not become synchronization anchors.

matcher = SequenceMatcher(
    lambda ch: ch in " \t",
    "name = value",
    "name=value",
)

Junk guides matching but does not erase differences from the output. If whitespace should be completely ignored, normalize text first and preserve originals for display.

Unified diffs

unified_diff() produces the format commonly used by patches and source reviews.

from difflib import unified_diff

before = "line 1\nold line\nline 3\n".splitlines(keepends=True)
after = "line 1\nnew line\nline 3\n".splitlines(keepends=True)

diff = unified_diff(
    before,
    after,
    fromfile="before.txt",
    tofile="after.txt",
)
print("".join(diff))

The function returns a generator. Convert it to a list only if the output must be reused or counted.

Preserving line endings

Use splitlines(keepends=True) for file comparison. Diff control lines include terminators by default so they work with writelines().

For inputs without line endings, pass lineterm="" to avoid inconsistent output.

Context diffs

context_diff() presents changed clusters in a before/after style.

from difflib import context_diff

for line in context_diff(before, after, fromfile="a", tofile="b", n=2):
    print(line, end="")

The n parameter controls how many unchanged lines surround a modification.

Detailed output with ndiff()

ndiff() prefixes every line:

  • - : unique to the first input;
  • + : unique to the second;
  • : shared;
  • ? : visual guidance for intraline differences.
from difflib import ndiff

result = list(ndiff(before, after))
print("".join(result))

Lines beginning with ? were not in either original and can be confusing when tabs are present.

Restoring text with restore()

An ndiff() delta can reconstruct either source.

from difflib import restore

original = "".join(restore(result, 1))
modified = "".join(restore(result, 2))

assert original == "".join(before)
assert modified == "".join(after)

This restoration works for Differ-style output, not arbitrary unified patches.

Differ for line-by-line comparison

The Differ class exposes similar output with optional line and character filters.

from difflib import Differ

comparator = Differ()
result = comparator.compare(before, after)
print("".join(result))

Differ does not claim to produce minimal deltas. Local matches are often easier for humans to understand than accidental distant synchronization.

Side-by-side reports with HtmlDiff

HtmlDiff generates a table or complete document with interline and intraline highlighting.

from difflib import HtmlDiff

html = HtmlDiff(wrapcolumn=80).make_file(
    before,
    after,
    fromdesc="Previous version",
    todesc="New version",
    context=True,
    numlines=3,
)

with open("comparison.html", "w", encoding="utf-8") as file:
    file.write(html)

This works well for editorial review and audit reports.

HtmlDiff security

fromdesc and todesc are interpreted as unescaped HTML. Escape user-provided values.

from html import escape

title = escape(user_supplied_name)

The generated report also contains document fragments and should be handled as potentially sensitive data.

Comparing bytes

diff_bytes() helps when encodings are unknown or inconsistent.

from difflib import diff_bytes, unified_diff

a = [b"old line\n"]
b = [b"new line\n"]

result = diff_bytes(unified_diff, a, b)
print(b"".join(result))

For known encodings, correct decoding remains preferable because it gives meaningful character-level differences.

difflib versus filecmp

difflib explains content differences. The filecmp module answers whether files or directory trees appear equal and can use metadata for a shallow comparison.

Use filecmp.cmp(..., shallow=False) to confirm content equality, then generate a textual report with difflib when needed. dircmp identifies common, unique, and differing files in directory trees.

Normalization before comparison

Depending on the goal, normalize Unicode, line endings, whitespace, case, and volatile fields.

import unicodedata


def normalize(text: str) -> str:
    text = unicodedata.normalize("NFC", text)
    return "\n".join(line.rstrip() for line in text.splitlines())

Over-normalization can hide important changes. Offer strict and tolerant comparison modes when appropriate.

Comparing JSON and structured data

Raw JSON comparison may report indentation and key-order changes. Parse it, serialize with stable ordering, and compare the normalized form.

import json

normalized = json.dumps(obj, sort_keys=True, indent=2, ensure_ascii=False)

For deep semantic changes, a structure-aware diff tool may be more appropriate.

Performance and limits

SequenceMatcher can have quadratic worst-case cost. Huge files and highly repetitive sequences require size, memory, and execution-time limits.

Do not accept unlimited documents in a synchronous API. Consider specialized external tools, background job infrastructure, sampling, or streaming approaches for large inputs.

Testing a similarity rule

def similar(a: str, b: str, threshold: float = 0.8) -> bool:
    return SequenceMatcher(None, a, b).ratio() >= threshold

assert similar("install", "instal")
assert not similar("install", "remove")

Build a labeled dataset of real examples to calibrate the threshold. Test empty strings, Unicode, repetition, whitespace, and reversed arguments.

Common mistakes

  • Treating ratio() as a probability.
  • Using difflib as semantic search.
  • Ignoring argument-order effects.
  • Comparing files without preserving line endings.
  • Rendering unescaped descriptions in HtmlDiff.
  • Disabling autojunk without benchmarking.
  • Loading unlimited files into memory.
  • Expecting a minimal edit sequence.

Best practices

  • Choose character, token, or line granularity.
  • Normalize only differences irrelevant to the domain.
  • Calibrate cutoffs with real data.
  • Use unified diffs for tools and HTML for people.
  • Escape metadata inserted into HTML.
  • Define size and time limits.
  • Use filecmp first when equality is enough.
  • Test Unicode, whitespace, and repeated sequences.

Conclusion

The Python difflib module turns sequence comparison into reports useful to people and programs. SequenceMatcher provides blocks, opcodes, and similarity scores; get_close_matches() suggests alternatives; and diff functions produce unified, contextual, detailed, and HTML formats.

Results improve when an application defines what counts as a meaningful change, chooses the correct granularity, and limits large inputs. With controlled normalization, safe HTML handling, and threshold tests, difflib provides a strong foundation for reviewers, validators, CLIs, and audit tools.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Chart dashboard representing statistical data analysis in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python statistics: Data Analysis Guide

    Learn Python statistics for mean, median, standard deviation, quantiles, correlation, regression, NormalDist, and KDE.

    Ler mais

    Tempo de leitura: 6 minutos
    31/07/2026
    Fraction charts representing exact rational numbers in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fractions: Exact Rational Numbers

    Learn Python fractions for exact rational arithmetic, automatic reduction, limit_denominator, formatting, and safe conversions.

    Ler mais

    Tempo de leitura: 5 minutos
    31/07/2026
    Calculator and documents representing precise Decimal calculations in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python Decimal: Precise Calculations

    Learn Python Decimal for exact calculations, money, quantize, rounding modes, contexts, and validation without float errors.

    Ler mais

    Tempo de leitura: 6 minutos
    30/07/2026
    Digital code representing unique and sortable UUID identifiers in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python uuid: Unique and Sortable IDs

    Learn Python uuid versions 4, 5, 6, and 7, validation, database storage, sortable IDs, and essential security practices.

    Ler mais

    Tempo de leitura: 7 minutos
    30/07/2026
    Organized files representing secure temporary storage in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python tempfile: Secure Temporary Files

    Learn Python tempfile to create secure temporary files and directories with automatic cleanup across Windows and Unix.

    Ler mais

    Tempo de leitura: 7 minutos
    29/07/2026
    Clocks representing international time zones with Python zoneinfo
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python zoneinfo: Time Zones Done Right

    Learn Python zoneinfo to convert time zones, handle daylight saving, fold, UTC, and tzdata without scheduling mistakes.

    Ler mais

    Tempo de leitura: 6 minutos
    29/07/2026