Bobobk

Practical Python, Linux and automation guides

Compare File Contents with Python filecmp: Avoid Shallow Checks and Stale Cache

2026-11-03 Technology
Compare File Contents with Python filecmp: Avoid Shallow Checks and Stale Cache
On this page

A backup and its source have identical sizes and modification times, yet Python says they match even when their bytes differ. The problem may be a metadata shortcut rather than a failed copy.

This beginner-to-intermediate tutorial reproduces shallow-comparison and cache pitfalls in filecmp.cmp(), then builds a read-only file-content comparison command. It distinguishes equality, differences, and failures for local backup checks or automation without deleting or overwriting either input.

1. Method: separate metadata from content

The official filecmp documentation explains the default shallow=True: matching file type, size, and modification time can establish equality without reading content. Explicit shallow=False disables that metadata-only equality shortcut; different sizes can still establish inequality immediately.

This tutorial compares regular files in trusted local directories after writers have stopped:

  1. Compare bytes without decoding text or ignoring whitespace, line endings, or a BOM. Do not compare permissions, owners, or timestamps.
  2. Use shallow=False and clear the process-local filecmp cache before each comparison.
  3. Use exit code 0 for equal content, 1 for different content, and 2 for argument or checking errors.
  4. Allow symlinks pointing to regular files. This is not a path-containment or hostile-input security tool.

To inspect which configuration lines changed, use the text configuration diff tool. Here the question is byte equality, not text equivalence.

2. Setup: create a demo directory

Use an existing Python 3.9 or newer interpreter; no third-party packages are needed. Examples were tested on Linux with Python 3.11.4, using bash/zsh-compatible shell commands. Choose a fresh directory name when repeating the experiment.

python3 --version
mkdir filecmp-demo
cd filecmp-demo

Save the next section as demo_filecmp.py. It creates and overwrites only left.bin and right.bin in the current demo directory; do not run it alongside real files with those names.

3. Reproduce: matching timestamps do not imply matching content

import filecmp
import os
from pathlib import Path

left = Path("left.bin")
right = Path("right.bin")
stamp = 1_700_000_000_000_000_000


def write_at_fixed_time(path, data):
    path.write_bytes(data)
    os.utime(path, ns=(stamp, stamp))


write_at_fixed_time(left, b"alpha\n")
write_at_fixed_time(right, b"omega\n")
filecmp.clear_cache()
print("default:", filecmp.cmp(left, right))
print("deep:", filecmp.cmp(left, right, shallow=False))

write_at_fixed_time(right, b"alpha\n")
filecmp.clear_cache()
assert filecmp.cmp(left, right, shallow=False) is True
write_at_fixed_time(right, b"omega\n")
print("cached deep:", filecmp.cmp(left, right, shallow=False))
filecmp.clear_cache()
print("cleared deep:", filecmp.cmp(left, right, shallow=False))

Run:

python3 demo_filecmp.py

Expected output:

default: True
deep: False
cached deep: True
cleared deep: False

We deliberately fix timestamps using os.utime(..., ns=...), giving equal-length files matching signatures. Timestamp-preserving copies can create similar situations in real workflows; waiting another second is not a fix for comparison logic.

cached deep: True demonstrates a second issue: this process first compares equal content, then changes the right file while retaining size and modification time. An earlier deep-comparison result can still be reused. The official clear_cache() documentation provides the reset mechanism. shallow=False does not disable the cache.

4. Implementation: a read-only comparison command

Save as compare_files.py:

"""Compare two stable local regular files by content. Python 3.9+."""
import argparse
import filecmp
from pathlib import Path
import sys


def same_content(left, right):
    for path in (left, right):
        if not path.is_file():
            raise ValueError(f"not a regular file: {path}")
    # Disable metadata-only equality and discard earlier comparison results.
    filecmp.clear_cache()
    return filecmp.cmp(left, right, shallow=False)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("left", type=Path)
    parser.add_argument("right", type=Path)
    args = parser.parse_args()
    try:
        equal = same_content(args.left, args.right)
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2
    print("SAME" if equal else "DIFFERENT")
    return 0 if equal else 1


if __name__ == "__main__":
    raise SystemExit(main())

In same_content(), shallow=False changes the equality decision, while clear_cache() avoids reusing earlier results. Clearing affects the entire process’s filecmp cache, so this small command is intended to run independently rather than manage a cache shared across threads.

The regular-file check follows symlinks. It catches common directory and missing-file inputs, but does not defend against concurrent path replacement. Do not use it on untrusted, constantly changing directories.

5. Usage and validation: inspect output and exit codes

Run in the same demo directory. The cp command only changes the demo’s right file to equal content; the comparison tool itself does not modify either file.

python3 compare_files.py --help
python3 compare_files.py left.bin right.bin; printf 'exit=%s\n' "$?"
cp left.bin right.bin
python3 compare_files.py left.bin right.bin; printf 'exit=%s\n' "$?"
python3 compare_files.py left.bin missing.bin; printf 'exit=%s\n' "$?"

Excluding --help, results appear in this order; the error message goes to standard error:

DIFFERENT
exit=1
SAME
exit=0
ERROR: not a regular file: missing.bin
exit=2

The demonstration intentionally returns 1 and 2. Do not paste it unchanged into a script using set -e. For automation, capture the status immediately and distinguish a difference from a failed check:

status=0
python3 compare_files.py left.bin right.bin || status=$?
case "$status" in
  0) printf 'verified same content\n' ;;
  1) printf 'content differs; review before continuing\n' ;;
  *) printf 'comparison failed; stop the job\n' >&2 ;;
esac

This demonstrates branching but does not terminate the surrounding job; production scripts should explicitly exit or return an error in failure branches. To track additions, missing files, and changes across a directory over time, use a SHA-256 dataset manifest.

Next, save the following as check_compare.py and run python3 check_compare.py. It uses an automatically cleaned temporary directory to check empty files, different lengths, equal content, the same path, non-UTF-8 bytes, a tail difference beyond 1 MiB, missing files, and directory inputs:

from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory

script = Path("compare_files.py").resolve()


def check(left, right, status, stdout):
    result = subprocess.run(
        [sys.executable, str(script), str(left), str(right)],
        capture_output=True, text=True, timeout=10,
    )
    assert result.returncode == status, result
    assert result.stdout == stdout, result
    if status == 2:
        assert result.stderr.startswith("ERROR:"), result
    else:
        assert result.stderr == "", result


with TemporaryDirectory(prefix="filecmp-check-") as name:
    root = Path(name)
    a, b = root / "a.bin", root / "b.bin"
    a.write_bytes(b"")
    b.write_bytes(b"")
    check(a, b, 0, "SAME\n")
    a.write_bytes(b"A")
    check(a, b, 1, "DIFFERENT\n")
    b.write_bytes(b"A")
    check(a, b, 0, "SAME\n")
    check(a, a, 0, "SAME\n")
    a.write_bytes(b"\x00\xff")
    b.write_bytes(b"\x00\xfe")
    check(a, b, 1, "DIFFERENT\n")
    a.write_bytes(b"x" * 1_048_576 + b"A")
    b.write_bytes(b"x" * 1_048_576 + b"B")
    check(a, b, 1, "DIFFERENT\n")
    b.write_bytes(a.read_bytes())
    check(a, b, 0, "SAME\n")
    check(a, root / "missing.bin", 2, "")
    check(a, root, 2, "")
print("OK: 9 CLI checks")

Expect OK: 9 CLI checks. Section 3 separately tests equal size/timestamps and same-process caching; ordinary equal-content cases alone do not cover those pitfalls.

6. Troubleshooting: unexpected results

Symptom Concrete action
Default comparison returns True for different bytes Pass shallow=False; do not substitute modification times for content checks.
Deep comparison still returns an old result Run filecmp.clear_cache() before the next comparison and ensure writers have stopped.
not a regular file Check the working directory and path. This example rejects directories, broken symlinks, and missing files.
PermissionError or another read error Use an authorized account and the correct path; do not hide an exception by reporting equality.
Text looks identical but returns DIFFERENT Byte comparison preserves CRLF, BOM, and final-newline differences. Use the text tool in section 1 when normalization is intended.
Results vary while files are being written Stop writers or compare completed independent copies. Clearing a cache does not provide locking or a consistent snapshot.

For duplicate candidates across a large directory, start with the read-only duplicate-file report instead of manually comparing every pair. Reading content can update access times; “read-only” means no content writes, not a guarantee that all metadata stays unchanged.

7. Summary

Before comparing files, decide whether metadata-only equality and cached results are acceptable. This command uses shallow=False with a cache reset before each comparison, then separates equality, differences, and errors with exit codes 0, 1, and 2. Stop concurrent changes first: it does not replace concurrency control, a complete backup-integrity strategy, or configuration semantic validation.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接