python-unidiff

A typed Python library that parses unified diff data into inspectable PatchSet, PatchedFile, and Hunk objects.

Library
PyPI
v1.0.0
286stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
77/100Good
Development Activity84
Maintenance72
Community72
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
67/100Good
Architecture78
Code Quality88
Innovation48
Learning Curve55

unidiff is a small, focused Python library for parsing and interacting with unified diff (patch) data. Feed it a diff — from git diff, hg diff, a .diff file, or any file-like/bytes source — and it returns a PatchSet: a list of PatchedFile objects, each a list of Hunk objects, each a list of Line objects, mirroring the actual structure of a diff.

Every level exposes the metadata you’d otherwise have to regex out yourself: per-file added/removed line counts, added/removed/renamed/binary file classification, git file modes and symlink detection, hunk source/target line numbers, and per-line added/removed/context flags with source and target line numbers. A metadata_only mode skips content tracking for faster parsing when you only need the stats.

The library also ships a small CLI (unidiff, or python -m unidiff) that prints a per-file addition/deletion summary from stdin or a file, useful as a quick diff-stat replacement in shell pipelines.

What You Get

  • PatchSet — parses from a file-like object, bytes, a string (PatchSet.from_string), or a filename (PatchSet.from_filename), and behaves like a list of PatchedFile objects
  • PatchedFile — per-file metadata: is_added_file, is_removed_file, is_modified_file, is_rename, is_binary_file, added/removed counts, and path
  • Git-specific metadata — source_mode/target_mode file modes (e.g. 100644, 120000) and an is_symlink shortcut, plus diff_line_no to locate hunkless binary entries
  • Hunk and Line objects with source_start/target_start, source_line_no/target_line_no, and is_added/is_removed/is_context flags for line-by-line inspection
  • Round-trip string rendering — str(patch) reconstructs the original unified diff text from any parsed object
  • A unidiff CLI (also python -m unidiff) that prints an addition/deletion summary per file, usable directly in a git diff | unidiff pipeline
  • metadata_only=True parsing mode that skips content tracking for faster stats-only parsing on large diffs
  • bytes input support and an explicit newline='\n' path for diffs containing embedded carriage returns or control characters

Common Use Cases

  • Code review tooling - annotate or comment on specific added/removed lines in a pull request diff by walking Hunk/Line objects
  • Coverage-on-diff / lint-on-diff - restrict a linter or coverage report to only the lines actually changed in a patch
  • Patch application scripts - inspect is_added_file/is_removed_file/is_rename before deciding how to apply a hunk to a working tree
  • Diff-stat CLIs - use the bundled unidiff command, or build a custom summary, from git diff | unidiff style pipelines
  • Changelog / release-note generation - walk added and removed lines across a PatchSet to summarize what changed between two revisions

Under The Hood

Architecture The library models a unified diff as four nested list subclasses that mirror the diff’s own structure: PatchSet (list of PatchedFile) → PatchedFile (list of Hunk) → Hunk (list of Line). Parsing lives in unidiff/patch.py as a hand-written line-by-line scanner (PatchSet._parsePatchedFile._parse_hunk) driven entirely by the compiled regexes centralized in unidiff/constants.py (git header, rename, mode-change, hunk-header, no-newline-marker patterns). This keeps pattern matching separate from the object model and from error signaling, which is isolated in unidiff/errors.py as a single UnidiffParseError. A metadata_only flag threaded through the parse path switches between full line-content tracking and a cheaper line-type-only scan, letting the same parser serve both “I need the exact diff back” and “I only need stats” callers without duplicating logic. __init__.py re-exports the public surface (PatchSet, PatchedFile, Hunk, constants) as the sole supported import path.

Tech Stack Pure Python 3.9+ with zero runtime dependencies — packaging is pyproject.toml/setuptools with a dynamic version pulled from unidiff/__version__.py. Type hints use modern PEP 585/604 generics (list[Line], Union/Optional from typing, from __future__ import annotations) and the package ships a py.typed marker for downstream type-checker consumption. The only extra surface is a stdlib-only argparse-based CLI in unidiff/__main__.py registered via [project.scripts].

Code Quality Tests (1,125 lines across test_parser.py, test_hunks.py, test_line.py, test_patchedfile.py) actually outweigh the library source (965 lines), run via stdlib unittest, and are checked against a real corpus of diff fixtures under tests/samples/ covering git, hg, bzr, quilt, and debdiff formats plus edge cases (symlinks, renames, embedded CR, quoted filenames). CI runs the suite across five Python versions (3.9–3.13) and layers a separate mypy type-check job over the source tree. Naming is consistent and each public property is documented inline; error handling is explicit via UnidiffParseError rather than silent failure on malformed input.

What Makes It Unique The object model is deliberately faithful to the diff format itself rather than flattening it into a generic dict/list structure, so callers can walk patch[i][j][k] and get exactly the file/hunk/line addressed by that position. Combined with the metadata_only fast path and the round-trip str() guarantee (parsed output regenerates byte-identical diff text), it functions equally well as a diff introspection library and as a diff-rewriting primitive, without pulling in any external dependency to do either.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search