difflib unified_diff() Glues Lines Together Without Trailing Newlines (tested)

difflib.unified_diff() expects each input line to already end with its own newline. We measured what happens when it doesn't, using the single most common way people accidentally strip them: str.split("\\n") instead of str.splitlines(keepends=True).

1. Lines without their own newline get glued together

import difflib

a = ["line1\n", "line2", "line3\n"]   # middle line has no trailing \n
b = ["line1\n", "line2 changed", "line3\n"]

diff = list(difflib.unified_diff(a, b, fromfile="a.txt", tofile="b.txt"))
print("".join(diff))
--- a.txt
+++ b.txt
@@ -1,3 +1,3 @@
 line1
-line2+line2 changed line3

Measured: the last three lines of the diff — the removed line, the added line, and the unchanged line after it — are fused into one garbled line with no exception and no warning. unified_diff() doesn't insert separators between entries; it assumes each line you gave it already carries its own trailing \n, and when one doesn't, the next entry just runs straight on.

2. The common way this happens: split() instead of splitlines(keepends=True)

text_a = "line1\nline2\nline3"
text_b = "line1\nline2 changed\nline3"

wrong_a = text_a.split("\n")                     # strips every newline
right_a = text_a.splitlines(keepends=True)        # keeps them

print("split():", wrong_a)
print("splitlines(keepends=True):", right_a)
split(): ['line1', 'line2', 'line3']
splitlines(keepends=True): ['line1\n', 'line2\n', 'line3']

Measured: text.split("\n") is the natural-looking way to turn a string into lines, but it discards every newline, including internal ones that unified_diff() relies on to separate its own output. splitlines(keepends=True) is the one that preserves what's needed (note even this leaves the very last line without a trailing \n if the source string didn't end with one — real diff tools handle that case with a "\ No newline at end of file" marker; Python's difflib does not emit one at all).

3. Fix: always build diff input with splitlines(keepends=True)

a_lines = text_a.splitlines(keepends=True)
b_lines = text_b.splitlines(keepends=True)
print("".join(difflib.unified_diff(a_lines, b_lines)))
---
+++
@@ -1,3 +1,3 @@
 line1
-line2
+line2 changed
 line3

Measured: once every line (except possibly the last) carries its own newline, unified_diff()'s output joins back into a correctly separated, readable diff with no further changes needed.

Frequently asked questions

Why does difflib.unified_diff() output look garbled?

We measured it happening when input lines don't each end with their own newline -- unified_diff() concatenates entries with no separator of its own, so adjacent lines run together in the joined output.

What's the right way to split text into lines for difflib?

We confirmed str.splitlines(keepends=True) preserves the newlines unified_diff() needs, while the more intuitive str.split('\\n') strips them and causes the glued-together output.

Does unified_diff() raise an error when lines lack newlines?

No -- we measured it silently producing malformed output with no exception, so the bug only shows up when you look at the printed diff, not at the point the problem was introduced.

Does difflib mark a missing trailing newline like real diff tools do?

No -- we confirmed Python's unified_diff() does not emit the '\\ No newline at end of file' marker that command-line diff/patch tools use; a missing final newline is simply not flagged.

Does this affect context_diff() too?

Yes -- context_diff() builds its output the same way, joining whatever lines it's given, so it has the identical gluing problem if the input lines don't already end in their own newlines.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in difflib: ran unified_diff() on lines produced by str.split('\n') (no trailing newlines) versus str.splitlines(keepends=True) (newlines preserved), then joined the output the standard way. PlainHeaders is our extension; Python's difflib module is built in.