Skip to main content

BlenDiff

Semantic diff, snapshot history, and assisted merge for Blender .blend files.

BlenDiff compares two .blend file states using Blender's Python API rather than binary diffing, and provides a full assisted merge system. It ships as both a Blender addon and a pip-installable pure-Python library for headless/CI use.

BlenDiff only reports changes it can back up, and only claims to apply what it actually applied. If an older snapshot never recorded a kind of data, that data is marked not compared instead of being diffed. Merge conflicts BlenDiff cannot write back are shown as read-only, so it never asks you to pick a value it would then throw away.


Features

  • Snapshot history: named, timestamped snapshots in a human-readable .blendiff JSON sidecar next to your .blend file, written atomically so a crash can never truncate your history, and content-addressed so a hundred snapshots of a mostly-unchanged scene cost a fraction of a hundred copies
  • Rename tracking: objects carry a persistent id, so renaming Cube to Body_LOW stays one modified object with all its other changes intact, instead of an unrelated delete plus add
  • Object & collection diffing: detects added, removed, renamed, and modified objects with per-property change tracking (local transform, viewport/render visibility, multi-collection membership)
  • Material node graph diffing: per-node, per-socket comparison including image names, input values, and rewired links
  • Render settings diffing: engine, resolution, sampling, output format, color management, Cycles and EEVEE sub-settings
  • Camera & light diffing: focal length, clip planes, DOF, sensor, light type, energy, shadow, spot/area/sun settings
  • Mesh geometry diffing: content digests for vertex positions, topology and UVs, so a moved vertex is detected even when every count and bound is unchanged, plus counts, bounding box, UV layers, shape keys, vertex groups
  • World/environment diffing: background color, strength, HDRI filepath, ambient occlusion
  • Modifier stack diffing: ordered comparison of 15+ modifier types with per-param change detection
  • Armature & pose diffing: bone hierarchy, rest positions, roll, deform and inheritance flags, bone collections, per-bone pose transforms, custom shapes, and bone constraints. Reparenting a finger, re-rolling a bone or rewiring an IK chain is now visible
  • Parent/child relationship diffing: parent name, parent type, parent bone (critical for rigs)
  • Constraint stack diffing: 25+ constraint types with per-param comparison (IK, Copy Location/Rotation/Scale, Track To, Child Of, and more)
  • Custom property diffing: detects added, removed, and changed obj[key] properties with float tolerance
  • F-curve diffing: per-channel keyframe count, frame range, interpolation, and extrapolation
  • Schema versioning: snapshots record which domains they captured, so diffing across BlenDiff versions never invents changes for a feature that did not exist yet
  • Three-way merge: conflict detection across every diff domain, with per-property resolution (Use A / Use B / Use Base) in a Blender UI that marks unappliable differences read-only
  • Annotated HTML export: self-contained dark-themed report with per-entry annotation textareas and JSON round-trip
  • Headless CLI: run diffs in CI without launching Blender

Installation

As a pip library (no Blender required)

pip install blendiff

As a Blender addon (Blender 4.2+)

Edit → Preferences → Get Extensions, search for BlenDiff, click Install. Updates arrive automatically.

As a Blender addon (Blender 3.6–4.1)

Download the latest blendiff-<version>.zip from Releases and install via Edit → Preferences → Add-ons → Install from Disk.


CLI Usage

# List snapshots in a sidecar file
blendiff list scene.blendiff

# Compare two snapshots
blendiff compare scene.blendiff "Before rigging" "After rigging"

# Fail CI if any changes exist
blendiff latest scene.blendiff --fail-on-changes

# Export an HTML report
blendiff compare scene.blendiff "v1" "v2" --output report.html

Python API

from blendiff.storage.sidecar import SidecarManager
from blendiff.diff_engine.diff_engine import DiffEngine

mgr = SidecarManager("scene.blendiff")
snaps = {s.label: s for s in mgr.list_snapshots()}

engine = DiffEngine()
result = engine.compare(snaps["v1"].data, snaps["v2"].data)

# Render settings diff
print(result.render_diff.summary())

# Object diffs
for diff in result.object_diffs:
    print(diff.name, diff.kind)
    for change in diff.changes:
        print(" ", change.property_path, change.old_value, "→", change.new_value)

# Renames, matched by persistent id rather than name
for diff in result.renamed_objects:
    print(diff.previous_name, "→", diff.name)

# Domains one snapshot never captured, so they were not compared
for note in result.skip_notes:
    print("not compared:", note)

# Parent relationship diffs
for diff in result.parent_diffs:
    print(diff.summary())

# Constraint diffs
for diff in result.constraint_diffs:
    print(diff.summary())

# Custom property diffs
for diff in result.custom_prop_diffs:
    print(diff.summary())

# F-curve diffs
for diff in result.fcurve_diffs:
    print(diff.summary())

Architecture

blendiff/
├── data_model/      # Dataclasses: SceneDiff, ConstraintDiff, plus schema.py
├── diff_engine/     # Pure comparison logic, no bpy
├── serializer/      # mathutils → JSON-safe types
├── storage/         # .blendiff sidecar CRUD + schema migration
├── merge_engine/    # Three-way merge and the applier registry
├── export/          # Shared diff→dict conversion + HTML report generation
├── cli/             # Headless CLI + importable Python API
├── extractor/       # bpy readers (Blender-only)
└── ui/              # Blender panels and operators (Blender-only)

The extractor is the only module that reads from bpy. Everything downstream is pure Python and fully testable without Blender. The merge applier writes to bpy, but only inside individual writer functions that import it locally, so the module stays importable and testable outside Blender.

What merge can and cannot apply

The applier registry (merge_engine/property_appliers.py) decides this, and can_apply(property_path) answers it directly. The merge UI uses that answer to decide whether to offer a choice at all, so it never asks you to pick between two values it would then throw away.

Applied automatically Reported, but reconcile by hand
Object name, local transform, rotation mode Mesh geometry (hashed, not stored)
Viewport and render visibility Modifier and constraint stacks
Collection membership Keyframes, drivers, NLA strips
Material slot assignment Material node graphs
Parenting (world position preserved) Object type, collection hierarchy
Pose bone transforms Bone constraints
Rest bones: parenting, rest pose, roll, flags Adding or removing bones
Custom properties Object creation (needs the source file)
Camera and light data

Snapshot compatibility

Snapshots record a schema version and the set of domains they captured. A domain captured by only one of two snapshots is reported as not compared rather than diffed. Otherwise a snapshot taken before F-curve support existed would make every curve in a newer snapshot look newly added. Older snapshots are migrated to the current schema as they are read; the file on disk is left alone until you call SidecarManager.migrate_file().


Running Tests

pip install blendiff[dev]

# Pure-Python core, no Blender needed
pytest tests/ -v -m "not integration"

# Everything, including the extractor tests that run inside Blender
pytest tests/ -v

1000+ unit tests run without Blender. A further 46 integration tests exercise the extractor package against a real Blender, and are skipped automatically when no Blender binary is found. Point BLENDER_BINARY at a specific build to test against a particular version:

BLENDER_BINARY=/opt/blender-4.2/blender pytest tests/integration -v

CI Integration

# .github/workflows/diff.yml
- name: Check for scene changes
  run: blendiff latest scene.blendiff --fail-on-changes

See docs/ci_example.yml for a diff-checking workflow template, and .github/workflows/tests.yml for the project's own CI: unit tests across Python 3.10–3.12, integration tests against Blender 4.2 and 5.1, and a check that the published wheel imports without Blender.

Releasing

Releases are fully automated. Bump __version__ in blendiff/__init__.py, update CHANGELOG.md, then push a tag:

git tag -a v0.9.0 -m "v0.9.0"
git push origin v0.9.0

.github/workflows/release.yml then verifies the tag matches the package version, runs the tests, builds the distributions, publishes to PyPI via Trusted Publishing (no API token), and attaches the Blender addon zip to the GitHub release with notes taken from the changelog.


License

GPL-3.0-or-later. See LICENSE.

BlenDiff is licensed under the GNU General Public License v3 or later because Blender's Extensions Platform requires it: anything using the bpy API is treated as a derivative of Blender, which is itself GPL. The pip package and the Blender addon ship the same code under the same terms.

Metadata

Release files for blendiff 0.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blendiff 0.8.1
File Size Uploaded
blendiff-0.8.1.tar.gz 213.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for blendiff 0.8.1
File Interpreter ABI Platform
blendiff-0.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 361.5 kB

Release files / blendiff-0.8.1.tar.gz

Download URL blendiff-0.8.1.tar.gz
Size 213.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a723401430923ead4954a4afb56b252e761b8ccc0347447a1ebb74e17d89d18e
BLAKE2b-256 checksum
How to use checksums
2654c5d38c89eb89cc1bc997a78de164b25fa87bd6f5d9a99b5292d7465a4d22
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release files / blendiff-0.8.1-py3-none-any.whl

Download URL blendiff-0.8.1-py3-none-any.whl
Size 147.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e5f685070250cf8296491414ebe36d2c7f5f950c6d926c49afa5c09cb888725d
BLAKE2b-256 checksum
How to use checksums
4e7c91ae8d825b6087fa5bfa8ba5d4d59165695fb52c190de01f3649e47a8ea1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.3

2 release files

0.8.2

2 release files

This release

0.8.1 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page