# Dulwich Deep Analysis: A Git Implementation Written in Python

> Dulwich reads and writes Git repositories without the git binary and without native code, then quietly adds Rust when speed matters. That combination makes it useful in places where shelling out is impossible and where you still need wire format compatibility with C Git.

**jelmer/dulwich** — Pure-Python Git implementation

- Repository: https://github.com/jelmer/dulwich
- Website: https://www.dulwich.io/
- Stars: 2,284 · Forks: 457
- Language: Python
- License: NOASSERTION
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/jelmer-dulwich

## Why a second Git implementation exists in Python

Most Python code that touches Git shells out. GitPython runs the git executable and parses what comes back. pygit2 wraps libgit2, which means a compiled library has to be present before your program can start. Both approaches are reasonable, and both leave a gap: code that needs to work on machines where git is genuinely unavailable, or inside a frozen application bundle where shipping a binary is awkward, or on a server whose only job is to serve objects out of a bucket.

Dulwich targets that gap directly. The project states that it provides an interface to Git repositories, local and remote, that does not call out to git and instead uses pure Python, and it contrasts itself with other Python libraries by noting that it ships as a standalone package with no dependency on git being installed and no native code required. The project documentation is candid that this comes at the cost of speed, and that the reason you accept it is easier deployment in environments where git is not available or where a pure Python implementation matters.

The scale of adoption is real but not enormous: GitHub reports roughly 2,284 stars, 457 forks, and 49 open issues on the repository. The topics are exactly what you would expect, including version-control, python, pypy, and cpython. The most recent push GitHub reports is dated 2026-09-23, and the release cadence backs that up, with tagged versions landing in August and September 2026. Jelmer Vernooij is the long running author, and the commit history shows a small set of regular outside contributors alongside him.

## Two layers: porcelain and the object graph

Dulwich exposes a lower level API and a higher level plumbing layer called porcelain, and the documentation shows both approaches side by side on the same operation. The lower level path opens a repository, asks for the head, indexes the object database by that id, and reads the commit message attribute. The porcelain path takes a path and a limit and prints a log.

```python
>>> from dulwich.repo import Repo
>>> r = Repo('.')
>>> r.head()
'57fbe010446356833a6ad1600059d80b1e731e15'
>>> c = r[r.head()]
>>> c
<Commit 015fc1267258458901a94d228e39f0a378370466>
>>> c.message
'Add note about encoding.\n'
```

That snippet contains a small inconsistency worth knowing about before you copy it into your own tooling. The head call returns a sha beginning 57fbe010, but the repr of the commit object retrieved under that same key shows a sha beginning 015fc126. Those cannot both be the head commit, so the example output appears to have been edited at two different times. The practical consequence is simple: when you need the authoritative object id, read the id attribute directly rather than parsing the repr. Any code that scrapes object hashes out of a repr will eventually be wrong.

The examples directory is arguably more telling about intended use than the tutorial snippet is. It ships clone, config, diff, filter_branch, merge_driver, memoryrepo, gcs, auth_callback, latest_change, and rename-branch scripts. A memory repository implementation and a Google Cloud Storage script together describe the use case that most distinguishes Dulwich from the alternatives: a program that manipulates Git objects as data, in memory or in object storage, without ever writing a working tree or invoking a subprocess.

A dependency on urllib3 at version 2.2.2 or newer is the only unconditional runtime requirement, with a conditional typing extensions dependency for Python versions before 3.12. The package requires Python 3.10 or later, and the documentation says the project supports and is tested on CPython 3.10 and later along with PyPy. The project metadata lists classifiers through Python 3.14, which is consistent.

## The Rust story, and why the pure Python label needs reading carefully

The headline description on GitHub reads Pure-Python Git implementation, and the opening paragraph of the documentation says pure Python repeatedly. Meanwhile the default build compiles Rust. Both statements are true at once, and the documentation explains the arrangement: by default the setup script attempts to build and install optional Rust extensions because they significantly improve performance, since low level operations that run often are much slower in CPython. If you do not want them, you ask for a pure build.

The extension list is short and specific. Three modules are built, each through PyO3 from a crate under the workspace:

```python
        rust_extensions = [
            RustExtension(
                "dulwich._objects",
                "crates/objects/Cargo.toml",
                binding=Binding.PyO3,
                optional=optional,
            ),
```

The Cargo workspace declares its members as a crates glob and pins the Python binding crate across a version range, and the workspace package version tracks the Python release, sitting at 1.2.15 to match the latest tag. In other words the Rust code is not a separate side project with its own version line. It ships in lockstep with the Python package, which means an accelerator bug and a Python bug are fixed in the same release.

There is also an escape hatch for constrained build environments. The setup script checks the CIBUILDWHEEL environment variable and treats PURE or a --pure argument as a signal to skip the extension build entirely.

```bash
    $ pip install --no-binary dulwich dulwich --config-settings "--build-option=--pure"
```

That install line is worth copying exactly rather than paraphrasing, because the flag names differ between a direct setup invocation and a pip invocation, and the documentation also shows how to record the same choice in a requirements file using the config settings form. If your build image has no compiler, you have three reasonable options: install from source with --pure, disable extensions at build time, or consume wheels that were already built elsewhere. Deciding this early avoids a confusing failure at deploy time.

## Compatibility with C Git is documented, not asserted

The compatibility claim in the documentation is specific: Dulwich aims to provide full wire format and repository format compatibility with C Git while keeping the implementation pure Python and free of a git dependency, and it states that Dulwich and C Git can be used interchangeably on the same repository. Interchangeability is the operationally important half of that sentence. It is what lets you read a repository that C Git created, write objects C Git understands, and hand the directory back without a repair pass.

The documentation does not stop at the claim. It points at a file, docs/c-git-compatibility.txt, described as a detailed list of which Git commands and features are supported. That is the document to read before committing to Dulwich for a workflow, because the gaps tend to live in the long tail of plumbing behavior rather than in the mainstream operations. Recent release notes show this is a moving surface: version 1.2.15 added handling that rejects reftable tables list entries containing a path separator or an absolute path, which implies reftable parsing is comparatively recent work rather than a decade of hardening.

For developers building on top of Dulwich rather than using it as a drop in replacement, the package metadata offers a broad set of optional dependency groups, each mapping to a capability.

```toml
[project.optional-dependencies]
fastimport = ["fastimport"]
https = ["urllib3>=2.2.2"]
pgp = ["gpg"]
paramiko = ["paramiko"]
colordiff = ["rich"]
```

Selecting these deliberately is worth the minute it takes. The pgp group pulls in the gpg module for signature verification. The paramiko group enables SSH transport. The aiohttp group, listed further down in the same table, switches transport to asyncio. Each extra widens the dependency surface and the number of CVEs you inherit, so a minimal install plus one extra is usually a better starting point than installing everything and pruning later. The console entry point declared in the package metadata is a single dulwich command bound to the module main function, which is what makes the porcelain layer reachable from a shell.

## Recent releases read like a security and performance audit

Reading the release notes for the three most recent versions is the fastest way to understand what the maintainers consider important. The pattern is unusually consistent: hostile input handling, removal of quadratic behavior, and thread safety.

The 1.2.14 release, published 2026-08-28, is a single entry. It collapses consecutive globstar segments during wildmatch translation to match git behavior and to avoid catastrophic regex backtracking on patterns of the form a slash star slash star slash star slash z arriving from untrusted repositories. The 1.2.15 release, published 2026-09-14, opens with a fix for delta cycles in pack resolution, where a crafted pack whose REF_DELTA objects named each other sent the raw object getter into an unbounded loop; such chains now raise a dedicated exception. It also rejects reftable entries that would let a repository make a ref lookup open a file outside the reftable directory.

Version 1.2.13, published 2026-08-24, is mostly concurrency and protocol behavior. It makes concurrent raw object access thread safe by synchronizing the pack offset cache, makes pack data reads thread safe by mapping pack contents and indexing the mapping at explicit offsets instead of sharing a file position, adds an identity sanitizer that mirrors how git formats identities for callers who cannot reject bad input, and bounds fetch negotiation the way C Git does by giving up after a fixed number of unacknowledged have lines instead of walking the entire graph. It also adds commit message reuse flags to the command line client, copying message, author, and author date from an existing commit.

Two conclusions follow for anyone deciding whether to upgrade. If your process opens repositories you did not create, the globstar and delta cycle fixes are the reason to move to 1.2.14 or later rather than staying on an older pin. If your service reads packs from several threads, the 1.2.13 offset and mapping work is the relevant boundary. Contributor credits name Jelmer Vernooij, Bojan Zivanovic, and netliomax25-code across these entries, which suggests a small project with an active outside security contributor.

## Reading the repository layout

The top level of the repository tells you what the project considers part of its job. The Python package sits in dulwich/, the Rust crates sit in crates/ with a workspace manifest at the root, and examples/ holds the ten scripts described earlier. Documentation has its own directory with a Read the Docs configuration and a Makefile target for building it, and the project points readers at a hosted copy as well.

Two directories stand out as effort signals. There is a fuzzing/ directory, which lines up with the atheris dependency group declared in the package metadata, and there is a property_tests/ directory. Libraries at this scale often skip both, and their presence is consistent with a project that has been feeding malformed packs and malformed ignore patterns into its parsers on purpose.

The supporting files show the review apparatus: a strict type checking configuration and a pyright configuration, a linting and type checking dependency group with pinned versions, a spell checking configuration, a coverage configuration, a test runner configuration, and a static analysis configuration. There is a checked in dulwich.cfg for project specific settings, plus small declarative files that look like build and status metadata. A SECURITY.md sits next to a CODE_OF_CONDUCT.md and a CONTRIBUTING file, and the documentation points contributors at the issue tracker.

One metadata point deserves a flag. The documentation states the license as Apache License version 2 or GNU General Public License version 2 or later, with an SPDX identifier expressing the dual choice, and the package metadata carries the same dual expression. GitHub, however, reports the repository license field as NOASSERTION, which is the detector declining to resolve a disjunction into one license rather than a claim that no license applies. If license clarity matters for your compliance process, read COPYING and the SPDX header carried in the packaging files instead of trusting the platform field, and if you need a single license to report upward, decide which branch of the dual grant you intend to rely on before shipping.

## Conclusion

Dulwich earns its place by being the option that does not need the git binary at all, and by publishing a compatibility document instead of a compatibility slogan. The trade is explicit and manageable: pure Python costs speed, the Rust extensions exist to claw some of it back, and the release history shows the maintainers treating untrusted repositories as a first class threat model rather than an afterthought.

## FAQ

### Is Dulwich a drop in replacement for the git command line tool?

Not entirely. Dulwich aims for full wire format and repository format compatibility with C Git, and it points to docs/c-git-compatibility.txt for the detailed list of supported commands and features. Treat that file, not the compatibility claim, as the source of truth for your workflow, and check it again after each release since new features land regularly.

### Does installing Dulwich require git to be present on the machine?

No. That is the core design goal. Dulwich implements Git in Python so it can run where the git binary is unavailable, inside frozen application bundles, and in minimal container images. The one caveat is that a compiler is wanted by default, because the optional Rust extensions are built unless you pass --pure.

### How do I install Dulwich without the Rust extensions?

Pass the pure option so setup skips the Rust build. The documentation shows the pip form using the no binary flag for dulwich plus a build config setting, and also shows how to record the same choice in a requirements file. The project also honors a PURE environment variable and skips extensions when the CIBUILDWHEEL marker is set.

### What is the fastest way to learn the Dulwich API?

Start from the two layers the documentation presents: the lower level repository API for object access, and the porcelain module for command style operations such as log. Then read the ten scripts in examples/, since memoryrepo.py, gcs.py, and clone.py show the object level usage that most distinguishes Dulwich from GitPython.

### Is Dulwich safe to run against repositories from untrusted sources?

The recent release history suggests the maintainers treat that as a primary threat model. Version 1.2.14 fixed catastrophic regex backtracking from untrusted globstar patterns, and 1.2.15 added delta cycle detection plus rejection of reftable paths that could escape the reftable directory. Staying current on those releases is the meaningful precaution.

### Why does GitHub show NOASSERTION for the Dulwich license?

Because the project is dual licensed and the platform detector does not resolve a disjunction into a single identifier. The documentation and the package metadata both state Apache License version 2 or GNU General Public License version 2 or later, expressed as an SPDX identifier with OR between the two. Read the COPYING file and the SPDX header in the packaging files for the authoritative statement.

## Sources

- [Issues](https://github.com/jelmer/dulwich/issues)
- [jelmer/dulwich on GitHub](https://github.com/jelmer/dulwich)
- [Project website](https://www.dulwich.io/)
- [README](https://github.com/jelmer/dulwich/blob/main/README.md)
- [Releases](https://github.com/jelmer/dulwich/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jelmer-dulwich
