Repository navigation
Difflib creates unreasonably large diffs #118150
Copy link
Copy link
Closed
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
#!/usr/bin/python3
import difflib
def get_lines(filename):
with open(filename, 'r', encoding='utf8') as fd:
return fd.readlines()
old_new = list(difflib.unified_diff(
get_lines('external.old'),
get_lines('external.new'),
))
new_old = list(difflib.unified_diff(
get_lines('external.new'),
get_lines('external.old'),
))
print('diff -u external.old external.new.json | wc -l')
print(len(old_new))
print('diff -u external.new external.old.json | wc -l')
print(len(new_old))$ ./difftest.py
diff -u external.old.json external.new.json | wc -l
30854
diff -u external.new.json external.old.json | wc -l
26$ diff -u external.old.json external.new.json | wc -l
26$ diff -u external.new.json external.old.json | wc -l
26Running on RHEL9 with Python 3.9.18.
I have a JSON file that I'm diffing and I tend to get very large diffs when lines are removed from the file. When lines are added the produced diffs are simple. The JSON file is a list of DNS records.
external.old.json
external.new.json
Here are smaller files that reproduce the effect. In trying to create the smaller files I noticed that if I go too small then the diffs work fine. If you remove the "abcluster.eas.gatech.edu" from both files the diffs work. I could not get the file smaller.
small.external.old.json
small.external.new.json
CPython versions tested on:
3.9
Operating systems tested on:
Linux
Linked PRs
- gh-118150: warn in doc about results asymmetry for difflib junk #153892
- gh-118150: expose autojunk SequenceMathcher flag to public methods/functions of difflib #153947
- gh-118150: difflib: expose autojunk flag from SequenceMatcher to public methods and functions #153959
- [3.15] gh-118150: warn in doc about results asymmetry for difflib junk (GH-153892) #154658
- [3.14] gh-118150: warn in doc about results asymmetry for difflib junk (GH-153892) #154659
- [3.13] gh-118150: warn in doc about results asymmetry for difflib junk (GH-153892) #154660
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
terryjreedy commentedon Apr 22, 2024
Can you illustrate the issue with much smaller strings, perhaps 20 or fewer lines included in the code, the instead of the 340Kb files attached?
Sxderp commentedon Apr 22, 2024
I updated the first post with smaller JSON. Unfortunately I couldn't reproduce the issue when I tried to make even smaller files. A size issue?
blhsing commentedon Apr 23, 2024
This is because of the
autojunkoption that isTrueby default. With this option enabledSequenceMatcherwould automatically consider "popular" elements (those that account for 1 + 1% of the total elements) as junk when the size of the second sequence is 200 or more. The intention is to speed up the diff by ignoring elements that repeat too much--at the cost of accuracy.You code would work as intended if you patch
SequenceMatcherwithautojunk=False:IMHO the
autojunkoption should be made available to all ofdifflib's utility functions includingunified_diff,diff_bytes,ndiff, etc., or be made into a global setting in thedifflibmodule, so one does not need to resort to patchingSequenceMatcherto disableautojunk.Sxderp commentedon Apr 23, 2024
This sounds like a reasonable ask. I concur.
terryjreedy commentedon Apr 23, 2024
Did either of you verify that autojunk=False actually solves the issue for the posted data?
@tim-one Proposal in #118150 (comment): make SequenceMatcher
autojunkoption available to utility functions that use SequenceMatcher. Or make option a module global. Reasonable? Which better? Better alternative?Sxderp commentedon Apr 23, 2024
1.a It is a fair bit slower.
1.b I find it strange that the autojunk errors are only triggering when lines are removed but it behaves when lines are added.
With my not-very-scientific tests
~ 5.46 autojunk = False (Unsure how much may be overhead from patch itself).
~ 0.02 default
~ 0.01 writing new files, diffing, deleting them
I don't expect to get anywhere close to diff speed and for my use-case 5 seconds doesn't matter (cron job).
terryjreedy commentedon Apr 23, 2024
For experiments, you should, depending on exact install type and location, be able to (temporarily) modify difflib.py itself.
Sxderp commentedon Apr 24, 2024
I copied the
difflib.pyfrom the system directory into my local directory and patched it. I'm still getting ~5s with autojunk = False. So patch was not providing a noticeable impact.The speed, at least for my use case, isn't too big of a concern. Being able to specify autojunk=False, by some means, to difflib is the most important short-term goal.
Lenormju commentedon Jul 20, 2025
The
SequenceMatcherexpects two parameters :a(set_seq1) andb(set_seq2). It could seem like it is symmetric, but the implementation definitely is not : thebserves as baseline, for which severalas can be compared to. With theautojunkon, I agree with the docs (Doc/library/difflib.rst) that it gives an output that human prefer. But this makes for a very surprising result : because it is only applied to only one side (B), it causes the diff to not be symmetric.It could look like a bug if it was not the intended behavior. (is it ?)
At the minimum, I think the docs could make it clearer that it can cause asymmetrical diffs, possibly larger than expected.
As discussed above, the
SequenceMatcher'sautojunkargument is not supplied by anydifflibfunction, so always at its default parameter valueTrue. This can be a surprise, because theautojunkbehavior is only explained in the SequenceMatcher doc (at the top of the module page), so there is no indication of that behavior in the doc of the others difflib's functions.Moreover,
ndiffandHtmlDifffor example already accepts bothlinejunkandcharjunk, the other twoSequenceMatcherparameters (also having a default value). So it looks to me thatautojunkshould be added to the other functions too.I am working on a PR for these two facets.
Lenormju commentedon Jul 17, 2026
I ran a search for similar GitHub issues, and found out that the problem had already surfaced:
ratiomethod, but it is unrelated toautojunkbehavior, and the documentation for the ratio method received a patch to indicate this (commit 7dafbe8).autojunksurprising behavior raised, and both suggesting a documentation change, and allowing to pass theautojunkarg explicitely (instead of being implicitely always True).charjunk/linejunk(distinct fromautojunk) behavior, and contributed a doc clarification for them (commit c51da2b).autojunksurprising behavior, and suggesting at the end to clarify its role regarding performance and subjective quality.autojunkarg was not set to False, maybe making it clearer in the docs would have prevented the error?Citing Tim Peters from 2022 (#90825 (comment)):
Which I agree with, so here is the plan I suggest:
autojunkparameter to ALL top-level functions or classes' methods that otherwise would not supply it and thus be True. This requires changing the public interface to the module, but in a non-breaking way (adding a kwarg parameter with a default value, still True).The second issue (
autojunknot being surfaced publicly) also has already been commented here:difflib.find_longest_matchperformance with cache #106877 (comment) (closed): Tim agreeing theautojunkargument should not have beenTrueby default, but it is too late to change it\nbetweenHtmlDiff(strips them) andndiff()(does not) caused the unexpectedautojunk=Trueto produce large outputsautojunkissue was already discussed between 2008 and 2010 on BPO (https://bugs.python.org/issue2986), and then a PR landed on GitHub in 2019 (BPO migration?), but got closed because its author did not sign the CLA.Other related tickets:
autojunk=Falsewould be as fast asautojunk=True), but moved as a smaller increment in Composability ofdifflib#145209 (open). It has been stale for a few months now. This is a different, competing, way to solve the same issue.autojunkparameter in 2010, while fixing some bugs.pythongh-118150: difflib - fix NEWS issue with ref. not found
pythongh-118150: difflib - fix NEWS doc for ReST
pythongh-118150: difflib - remove template notes from NEWS
13 remaining items
pythongh-118150: update NEWS again
pythongh-118150: fix for sphinx linter
pythongh-118150: Merge branch 'main' into pythongh-118150-expose-auto…
pythongh-118150: clean whitespaces for lint
Merge branch 'main' into pythongh-118150-expose-autojunk
pythongh-118150: difflib - update doc
Merge branch 'main' into pythongh-118150-expose-autojunk
pythongh-118150: difflib - update doc
pythongh-118150: warn in doc about results asymmetry for difflib junk (…
pythongh-118150: difflib - update doc
gh-118150: difflib: expose autojunk flag from SequenceMatcher to publ…
encukou commentedon Sep 7, 2026
Thank you @Sxderp for the research, and @Lenormju & @faithlesstomas for the fixes!
pythongh-118150: difflib: expose autojunk flag from SequenceMatcher t…
force unified_diff to accept autojunk=False