Using Python’s difflib to Highlight Config File Changes
Configuration files are often edited under pressure: a service is failing, a security setting needs tightening, or a deployment must be adjusted before business hours. A small change to a port, access rule, or timeout can have consequences that are difficult to spot in a long YAML, INI, or Apache configuration file.
Python’s standard-library difflib module provides a practical way to compare two text versions without installing a third-party package. It can produce unified diffs for terminals, side-by-side HTML reports for browsers, and detailed change operations for custom tooling.
For Australian development teams, this approach suits everything from a Linux server in a Sydney data centre to a local test machine in Melbourne. It also supports review practices around the Australian Privacy Act and the Australian Signals Directorate’s Essential Eight, where configuration changes should be traceable and carefully checked.
A useful comparison tool should preserve the original files, make additions and removals obvious, and fit into existing scripts or deployment pipelines. With a few lines of Python, difflib can provide that foundation.
Reading Two Configuration Versions Safely
The simplest comparison begins by reading both files as text. Using UTF-8 explicitly avoids inconsistent behaviour between developer laptops, build agents, and Linux servers.
from pathlib import Path
old_text = Path("app.conf.old").read_text(encoding="utf-8")
new_text = Path("app.conf").read_text(encoding="utf-8")
old_lines = old_text.splitlines(keepends=True)
new_lines = new_text.splitlines(keepends=True)
splitlines(keepends=True) retains newline characters, which helps difflib produce clean output. It is also useful when a file has a missing final newline, since that detail may matter when reviewing generated configuration.
Before comparing files, restrict access to sensitive paths and avoid printing secrets. Configuration files commonly contain database passwords, API keys, certificates, or private connection details. A diff should be treated as sensitive operational data rather than harmless diagnostic output.
Producing A Unified Diff
A unified diff is compact and familiar to anyone who has reviewed a Git commit. It shows removed lines with a minus sign, added lines with a plus sign, and nearby unchanged context.
import difflib
diff = difflib.unified_diff(
old_lines,
new_lines,
fromfile="app.conf.old",
tofile="app.conf",
lineterm=""
)
print("".join(diff))
The fromfile and tofile labels make reports easier to understand when they are attached to a deployment ticket. The lineterm="" setting prevents extra blank lines when the source lines already include newline characters.
For a clearer command-line result, write the output to a file or pipe it through a pager. A system administrator working to Australian Eastern Standard Time can then inspect a change during a maintenance window without manually scanning two complete files.
Highlighting Added And Removed Lines
For a more explicit line-by-line display, difflib.ndiff() prefixes each line with a marker. A space means the line is unchanged, - identifies content present only in the old version, and + identifies content present only in the new version.
for line in difflib.ndiff(old_lines, new_lines):
print(line, end="")
This format can show intraline hints for similar lines, which is helpful when a value changes from timeout = 30 to timeout = 60. The output is less compact than a unified diff, but it can be easier for less experienced reviewers to interpret.
A custom highlighter can convert these markers into ANSI colours. Removed lines might appear in red and additions in green, while unchanged content remains plain. Colour should be optional because logs, CI systems, and some terminal environments do not interpret ANSI escape sequences correctly.
Creating A Browser-Friendly Report
difflib.HtmlDiff creates a side-by-side HTML table with highlighted changes. This works well when a team needs to review a generated report through an internal dashboard or attach it to a change record.
import difflib
from pathlib import Path
html = difflib.HtmlDiff(wrapcolumn=100).make_file(
old_lines,
new_lines,
fromdesc="Previous configuration",
todesc="Proposed configuration"
)
Path("config-diff.html").write_text(html, encoding="utf-8")
The resulting document is useful for long Apache, Nginx, systemd, or application files. A browser view is often more accessible than terminal output, particularly for teams distributed between Brisbane, Perth, and Canberra.
HTML output should still be handled carefully. Do not publish a report to a public web server if it may expose credentials, internal hostnames, customer identifiers, or network architecture. Sanitise sensitive values before generating the report, or restrict the file to an authenticated internal location.
Comparing Configurations Without Misleading Results
Text comparison is reliable only when equivalent files use consistent formatting. A reordered set of keys may appear as a large change even when the service behaves identically. Conversely, two files can look similar while representing different effective settings because of duplicate keys, includes, inheritance, or environment substitution.
For structured formats, parse and normalise the data before comparing it where practical. JSON can be loaded and emitted with sorted keys; INI files can be inspected through configparser; YAML requires an appropriate parser and careful treatment of anchors and types. Keep a raw text diff as well when whitespace, comments, and ordering are operationally meaningful.
Line endings also matter. Files edited on Windows may contain CRLF endings, while Linux tools generally expect LF. Normalising line endings before comparison reduces noise, but it should be reported separately if line-ending changes themselves are relevant to a deployment.
Turning The Diff Into A Review Tool
SequenceMatcher provides lower-level control when a project needs structured change data instead of display text. It can identify equal, deleted, inserted, and replaced blocks, allowing a script to produce JSON, audit events, or policy warnings.
from difflib import SequenceMatcher
matcher = SequenceMatcher(None, old_lines, new_lines)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag != "equal":
print({
"operation": tag,
"old_lines": [i1 + 1, i2],
"new_lines": [j1 + 1, j2],
})
A deployment checker could reject an unexpected change to PermitRootLogin, flag a newly exposed listening address, or require an approval when firewall rules are altered. This is valuable for Australian organisations aligning operational controls with Essential Eight practices.
Diff results can also be integrated with monitoring utilities. For example, Scratchy log analyser can help investigate log changes alongside a configuration review when a service begins producing unusual traffic or authentication failures.
Handling Large Files And Repeated Reviews
difflib is designed for general-purpose sequence comparison, not unlimited-scale data processing. Very large generated files may consume substantial memory and CPU, particularly when many similar lines are repeated. Comparing only relevant sections or using a repository’s native diff engine may be more efficient in that situation.
For routine administration, save a baseline copy and compare each proposed version against it. Include a timestamp, hostname, application version, and operator identity in the surrounding audit record, while keeping secrets outside the record. Australian businesses operating across daylight-saving boundaries should store timestamps in UTC and display local time only for human review.
A small Python wrapper can make this process dependable: validate both paths, normalise line endings, mask known secret patterns, generate a unified diff, return a non-zero exit code when changes exist, and fail clearly when either file is unreadable. That turns difflib from a quick comparison trick into a useful part of configuration governance.
