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

    Document and inbox representing local email stores with Python mailbox
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python mailbox: Local Email Stores

    Learn Python mailbox to read, create, and migrate Maildir, mbox, and MH stores with locking, flags, message parsing, and safe

    Ler mais

    Tempo de leitura: 5 minutos
    12/08/2026
    Text editor representing formatting with Python textwrap
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python textwrap: Format Text

    Learn Python textwrap to wrap, fill, shorten, indent, and dedent text with controlled width, whitespace, long words, and reusable settings.

    Ler mais

    Tempo de leitura: 4 minutos
    10/08/2026
    Folder and magnifying glass representing file-name filters with Python fnmatch
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fnmatch: Filter File Names

    Learn Python fnmatch to filter file names with shell wildcards, control case sensitivity, exclude patterns, and distinguish glob from regex.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Monitor with binary data representing compact numeric arrays in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python array: Compact Numeric Data

    Learn Python array for compact numeric storage, binary files, byte order, memory views, and safe buffer interoperability.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026
    Color wheel representing RGB, HSV, and HLS conversions with Python colorsys
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python colorsys: RGB, HSV, and HLS

    Learn Python colorsys to convert colors between RGB, HSV, HLS, and YIQ, generate palettes, and avoid scale and precision mistakes.

    Ler mais

    Tempo de leitura: 5 minutos
    09/08/2026
    Configuration icon representing plist files with Python plistlib
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    plistlib: Read and Write Apple Plist Files

    Learn Python plistlib to read and write XML and binary plist files, validate data, handle dates, bytes, and UIDs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026