LESSON 92 · DEEPER PYTHON29 min

Compare Text with Unified Diffs

Create a unified diff for two text versions with stable labels and preserved line endings.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Compare Text with Unified Diffs

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A diff helps a reader answer a simple question: what changed between two versions of a text? If one line is inserted near the top, the later lines move down. Comparing line one with line one, line two with line two, and so on produces noisy results because it mistakes a shift for many edits. Python's `difflib` contains sequence- comparison tools and a function that produces unified diff text similar to familiar code review patches. Today we will reproduce the noisy comparison, inspect which lines are equal, added, or removed, and build a small function that returns a readable unified diff.

  2. 00:40

    We will also test the no-change case so the function does not claim an edit where none exists. The demonstration uses in-memory strings, no repository and no shell command. The same reasoning applies to release notes, configuration changes, and a small editor. A diff is a view of changes, not a guarantee that the new text is semantically correct. We still need tests or human review for that. `zip` pairs items by index and stops when the shorter list ends. Once the extra line appears, old `beta` is paired with new `beta-extra`, and old `gamma` is paired with new `beta`. The final new `gamma` is not paired at all.

  3. 01:21

    The output makes one insertion look like two replacements, then silently drops the trailing item. There is nothing wrong with `zip`; it does exactly what it promises. It is the wrong model for finding insertions and deletions. We need to align sequences by matching common parts, then describe the gaps between them. A diff algorithm does not simply compare equal row numbers. Also note that a line can appear multiple times, so alignment can be ambiguous. For this tiny example there is a clear answer, but on a large repetitive file different reasonable alignments may produce different-looking diffs. The goal is a useful explanation of the change, not a magical proof of intent.

  4. 02:06

    `SequenceMatcher` finds matching blocks and describes the spaces between them with operation codes. Here the first line is equal, one new line is inserted, and the two later lines are equal again. That matches our mental model. We set `autojunk=False` so this small example is not affected by the heuristic that skips very common elements in longer sequences. For ordinary files, the default heuristic is often fine, but it is useful to know it exists. The indices in each operation refer to slices of the old and new lists. If you are building a custom change viewer, these operation codes can drive highlighting.

  5. 02:47

    For a human-readable patch, `difflib.unified_diff` does the formatting for us. The point of this diagnostic run is to confirm the alignment before formatting. If the program still claimed that `gamma` was deleted and re-added, we would know the issue was in the comparison, not merely in the display layer. The function feeds two sequences of lines into `unified_diff`. `splitlines(keepends=True)` preserves each line ending so the joined output has normal line breaks. The `fromfile` and `tofile` names are labels for the human reader; they do not open files. The hunk includes surrounding context and marks the new line with a plus sign.

  6. 03:29

    Unchanged lines appear without a plus or minus, which helps place the edit. If you see a line beginning with minus, that means the old version contained it and the new version does not. A unified diff is useful in a report or review, but it is still plain text. This function does not apply a patch, and it does not save a file. It only summarizes two strings already in memory. That distinction lets us test the comparison logic independently of file I/O. Try renaming the labels and see that the content comparison remains the same while the headers change. The no-change assertion is an important control.

  7. 04:09

    If the text is identical, the diff function returns an empty string rather than a header with a false edit. The next assertions check the intended insertion and guard against the noise from the broken `zip` approach. We do not compare every byte of the diff here; that could make a test overly sensitive to harmless context formatting. Instead, we assert the behavior that matters to our example: one added line and no false removals of `beta` or `gamma`. In a real change viewer, add cases for deletions, replacements, repeated lines, and files without a final newline. If the output will be used as an actual patch, test it with the patching tool you

  8. 04:51

    intend to use rather than assuming every display diff is directly applicable. On the lesson page, change the input strings and predict how the hunk will move. The skill to take away is choosing an alignment-aware tool for text changes, keeping line endings deliberately, and testing both a real edit and no edit.

Understand the concept

A text comparison is more useful when it shows which lines changed, rather than only whether two strings are equal. difflib.unified_diff produces the familiar --- before and +++ after headers, a @@ hunk header, context lines, removed lines prefixed with -, and added lines prefixed with +. It works on sequences of lines and does not need Git or a shell command.

Use splitlines(keepends=True) so each input line carries its original newline. unified_diff yields strings lazily; join them to return one report. Label the sides with fromfile and tofile so the reader knows what changed. If there is no change, the iterator yields nothing and the result is an empty string. This output is a report, not a guarantee that a patch can be applied to every arbitrary text file.

Line endings matter. If the final input line has no newline, difflib may display a line without a clean boundary before the next marker. Many tools add a special no-newline marker, but this simple lesson does not implement that convention. Preserve the original input rather than silently adding a newline and claiming the files were identical.

The task compares two small text strings supplied by the caller. It must not read local files from the visitor's computer. Tests cover a replacement, an insertion, and unchanged text. In a real application, read both files with an explicit encoding first, decide how to display binary files, and use a size limit for very large inputs.

  • Unified diffs mark removed and added lines
  • splitlines(keepends=True) preserves line breaks
  • unified_diff is a lazy iterator
  • Identical inputs produce an empty report

See it step by step

Read the code, predict the output, then compare it with the result.

01. Show one changed line in context

PYTHON
import difflib
before = 'alpha\nold\n'
after = 'alpha\nnew\n'
diff = ''.join(difflib.unified_diff(before.splitlines(keepends=True), after.splitlines(keepends=True), fromfile='before', tofile='after'))
print(diff, end='')
EXPECTED OUTPUT
--- before
+++ after
@@ -1,2 +1,2 @@
 alpha
-old
+new

The headers label versions; minus and plus indicate the replacement.

02. See an inserted line

PYTHON
import difflib
diff = ''.join(difflib.unified_diff('one\n'.splitlines(keepends=True), 'one\ntwo\n'.splitlines(keepends=True), fromfile='before', tofile='after'))
print([line for line in diff.splitlines() if line.startswith('+')])
EXPECTED OUTPUT
['+++ after', '+two']

The added line is marked +; the +++ line is a file label, not content.

03. Return nothing when nothing changed

PYTHON
import difflib
lines = 'same\n'.splitlines(keepends=True)
print(len(list(difflib.unified_diff(lines, lines))))
EXPECTED OUTPUT
0

An empty diff means there are no changes under this line comparison.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Read the markers in a unified diff

The first output labels the two versions with --- before and +++ after. A hunk header follows, then one unchanged context line, one removed line prefixed with minus, and one added line prefixed with plus. That format says more than a Boolean changed flag: it shows exactly where the replacement occurred.

difflib.unified_diff operates on sequences of lines and yields output lazily. Joining the yielded strings gives a report suitable for display. Preserve newline characters with splitlines(keepends=True), otherwise adjacent markers can run together and make the output hard to read. This exercise compares caller-provided text; it does not open visitor files.

PYTHON
import difflib
before = 'alpha\nold\n'
after = 'alpha\nnew\n'
diff = ''.join(difflib.unified_diff(before.splitlines(keepends=True), after.splitlines(keepends=True), fromfile='before', tofile='after'))
print(diff, end='')
EXPECTED OUTPUT
--- before
+++ after
@@ -1,2 +1,2 @@
 alpha
-old
+new
Follow the reasoning
  1. Split both texts while retaining line endings.
  2. Label the before and after versions.
  3. Interpret context, removal, and addition markers separately.
02 / 03

Separate file labels from added content

The second output collects lines beginning with plus. It includes +++ after as well as +two. The first is metadata naming the new version; the second marks actual inserted content. A viewer that treats every plus-prefixed line as an inserted text line would misreport the change.

Use the unified diff as a human-readable report unless you implement a fuller parser that knows hunk structure and file headers. Keep the original input intact, including whether it ends with a newline. Different diff tools may annotate missing final newlines differently, so this focused function should not promise a fully portable patch file.

PYTHON
import difflib
diff = ''.join(difflib.unified_diff('one\n'.splitlines(keepends=True), 'one\ntwo\n'.splitlines(keepends=True), fromfile='before', tofile='after'))
print([line for line in diff.splitlines() if line.startswith('+')])
EXPECTED OUTPUT
['+++ after', '+two']
Follow the reasoning
  1. Generate an insertion-only diff.
  2. Identify the +++ header separately from + content.
  3. Keep metadata and file lines distinct in any later UI.
03 / 03

Return an empty report for equal input

The final run compares one line with itself. unified_diff yields no entries, so its length is zero. The checked function should return an empty string for unchanged text instead of manufacturing a no changes banner inside the diff format. The caller can decide how to present that state.

Test one replacement, one insertion, and unchanged input. Make the labels stable so test output is reproducible. For local files, reading with an explicit encoding and limiting input size would be separate concerns. The browser task stays on small strings to teach the diff structure without filesystem or binary-data complications.

PYTHON
import difflib
lines = 'same\n'.splitlines(keepends=True)
print(len(list(difflib.unified_diff(lines, lines))))
EXPECTED OUTPUT
0
Follow the reasoning
  1. Compare identical line sequences.
  2. Join the empty diff iterator to an empty string.
  3. Keep display messaging outside the report-producing function.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnMake make_diff(before, after) return ''.join(difflib.unified_diff(...)) using splitlines(keepends=True), fromfile='before', and tofile='after'. The prepared call must print the six expected diff lines; identical inputs return an empty string.
Need a hint?

Pass before.splitlines(keepends=True) and after.splitlines(keepends=True) as the first two arguments of difflib.unified_diff, then join the iterator.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does a line beginning with - inside a unified diff mean?

    02.Why use keepends=True?

    03.What does unified_diff yield for identical lines?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Dropping all line endings

    COMMON MISTAKE
    before.splitlines()
    FIX
    before.splitlines(keepends=True)

    What happens: Concatenated diff output can put markers on the same line.
    Preserve newline characters on input lines.

    Treating every + as added content

    COMMON MISTAKE
    added = [line for line in diff if line.startswith('+')]
    FIX
    skip the first two file headers before counting content lines

    What happens: The +++ after file header is counted as an added line.
    Unified diff has metadata as well as content markers.

    Browse this lesson’s problem fixes

    Sources and further reading