# Griffe extracts your Python API, then tells you what you broke

> Griffe is a static analysis library for Python that extracts the structure of a project, serializes it as JSON, and compares two versions of an API to report breaking changes, from the command line or from Python code. It is published under an ISC licence, it needs Python 3.11 or newer, and it is the analysis layer behind the mkdocstrings documentation toolchain.

**mkdocstrings/griffe** — Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API.

- Repository: https://github.com/mkdocstrings/griffe
- Website: https://mkdocstrings.github.io/griffe
- Stars: 701 · Forks: 79
- Language: Python
- License: ISC
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mkdocstrings-griffe

## A static reader for the shape of a Python project

Griffe describes itself with a phrase that is unusual for a documentation tool: signatures for entire Python programs. The framing is that of an anatomy tool. You extract the structure, the frame, the skeleton of your project, and from that skeleton you can generate API documentation or find breaking changes in your API.

The keyword list in the package metadata is more telling than the marketing line. It includes static-analysis and dynamic-analysis alongside api, signature, breaking-changes and docs, which tells you that both routes into a program's shape are on the table, and that the primary output is a description of the API rather than rendered documentation.

The name explains the idea. Griffe, pronounced grif, is a French word that means claw, but also signature in a familiar way, and the README quotes the line about recognising a griffe. The project lives under the mkdocstrings organisation, its documentation is hosted at mkdocstrings.github.io/griffe, and the topic list includes mkdocstrings-collector, which is the clearest sign that its main consumer is a documentation toolchain that needs a collector rather than a renderer.

For anyone deciding whether to adopt it, the practical question is what it does that a plain import and inspect does not, and the answer is in the loading and checking commands.

## griffe dump turns a package into JSON

The command line entry point for inspection is griffe dump, and it takes the names of the packages to dump. The output is JSON, one object per package.

```console
$ griffe dump httpx fastapi
{
  "httpx": {
    "name": "httpx",
    ...
  },
  "fastapi": {
    "name": "fastapi",
    ...
  }
}
```

That is the whole interface, and it is deliberately boring. What comes back is a serializable description of the API, which means it can be stored, diffed, checked into a repository, or fed to another tool. The documentation points to a Serializing chapter for more examples.

The value of a JSON dump over an introspection dump is that the two are comparable across time. If a tool consumes this structure, then a change to a function signature shows up as a change in a file you can diff, which is a different kind of artefact from a trace of a running program.

This is also the seam where Griffe stops being a documentation helper. A serialised API is a contract description, and a contract description is exactly what you need to answer a question like whether a release broke somebody.

## griffe check reports the parameter kind change that breaks callers

The checking command is the reason the project is not only a documentation helper. You pass a relative path to a package, and Griffe compares the current state of that package against a reference and prints what changed.

```console
$ griffe check mypackage --verbose
mypackage/mymodule.py:10: MyClass.mymethod(myparam):
Parameter kind was changed:
  Old: positional or keyword
  New: keyword-only
```

The output format is designed to be read as a report rather than as a stack trace. Each finding gives a file and line, the qualified name of the member, the kind of change, and the old and new values. This is a change a human reviewer will recognise immediately as a breaking change, because a parameter that was callable positionally has become keyword-only.

The verbose flag turns the report up in detail. Projects with a src layout need one extra option, since the package does not sit where the search would otherwise begin.

```console
$ griffe check --search src mypackage --verbose
src/mypackage/mymodule.py:10: MyClass.mymethod(myparam):
Parameter kind was changed:
  Old: positional or keyword
  New: keyword-only
```

The point of a tool like this is to run it in continuous integration, where a change like the one above is a regression rather than a surprise. Griffe does not need your tests to fail on it, because the signature change is the evidence.

## Comparing against a published release needs the pypi extra

Checking a local package against a git tag is one thing, but the question most projects actually have is whether the new release breaks the version on PyPI. Griffe can fetch packages from PyPI.org, or from other indexes configured through the PIP_INDEX_URL environment variable.

That capability is not in the base install. It requires the pypi extra.

```bash
pip install griffe[pypi]
```

The command syntax then takes a package name with a baseline and a candidate version, given with the b and a flags.

```bash
griffe check package_name -b project-name==2.0 -a project-name==1.0
```

The baseline and the candidate are named separately, so the same package can be checked against any published version you can install. That is the difference between reviewing a diff and reviewing an API surface: the diff shows you the lines that changed, the API surface shows you the calls that stopped working.

The extra also implies a trust boundary. Installing from a package index during a check means the comparison depends on what that index serves, and a private mirror configured through PIP_INDEX_URL changes the reference point. Worth knowing before wiring it into a release pipeline that runs unattended.

## The same three operations from Python

Everything the command line does is available as a library, and the Python API is where the tool becomes a component.

```python
import griffe

fastapi = griffe.load("fastapi")
```

Loading a package gives you the model in memory, and the checking operation is available as a function over two models.

```python
import griffe

previous = griffe.load_git("mypackage", ref="0.2.0")
current = griffe.load("mypackage")

for breakage in griffe.find_breaking_changes(previous, current):
    ...
```

The load_git call takes a package and a ref, so the previous state of the project is read from a git reference rather than from a checkout you have to arrange. The function returns an iterable of breakages, and the example leaves the handling open, which is the right shape for a library: the caller decides whether a breakage is a warning, a log line or a non-zero exit code.

This is the difference in approach against a runtime introspection tool, and it is worth being precise about it. Introspection imports your code and asks the live objects what they look like, which gives you accurate runtime defaults and values, but it needs the package to be importable, with all of its dependencies installed, in the environment where you run it. Static extraction reads the source, so it works on code that cannot be imported, on a different Python version than the project targets, and on a repository checkout with no environment set up at all. The price is that what the source says is what you get, not what the running object says.

## Packaging that assumes a modern Python and a toolchain first

The project metadata says requires-python >=3.11, and the classifier list runs from Python 3.11 through 3.15, with the older entries marked in comments for removal at end of life. The distribution declares itself typed and marks itself as production stable.

The build system is hatchling, with pdm-backend left in place as a dependency of the version discovery script in use, and uv-dynamic-versioning in the requires list, which is how the version is derived rather than hardcoded. The version itself, along with the dependencies and the optional dependencies, is declared dynamic, so the packaging metadata is generated rather than written by hand.

That last point is a small maintenance cost with a clear payoff. A tool whose whole job is to describe other people's packages has to be able to describe a package installed from a source distribution, and dynamic version discovery is part of making that work.

The repository layout is consistent with a tool that is used as much as it is read. Alongside the source there are packages/, docs/, config/, scripts/, a duties.py file that holds the project tasks, a Makefile whose only job is to hand arguments to a scripts/make shell script, an .envrc for direnv, and a CHANGELOG.md. There is also an AI_POLICY.md, a CODE_OF_CONDUCT.md and a CONTRIBUTING.md, and a Radicle clone badge, which suggests the project is also published through a peer to peer git remote.

## An ISC licence and a release cadence measured in days

The licence is ISC, declared in the project metadata with the LICENSE file shipped alongside, which is a short permissive grant with attribution as the main obligation. It is a good fit for a library that other tools embed, because there is no copyleft obligation that would push downstream users toward a particular packaging decision.

The release history shows a project that is being shipped continuously. Version 2.4.0 was published on 2026-10-19, with 2.3.2 and 2.3.1 both appearing on 2026-10-06, an hour apart. The last push to the main branch was on 2026-10-19, the same day as 2.4.0.

Two 2.3.x releases an hour apart on the same day is a sign of a fast patch and republish cycle rather than a slow-moving library, and it has a practical consequence for anyone who depends on it. Pinning to a minor version is not enough to guarantee a stable target, and a changelog has to be read rather than skimmed.

The project keeps a CHANGELOG.md at the root and publishes a changelog URL in its metadata, and it also exposes a Radicle seed, which gives a second distribution channel for a library whose consumers are documentation toolchains that are themselves often vendored into other projects. For an adoption decision, the licence is settled and the remaining question is operational: which version do you pin, and how quickly do you need to move when the next one lands.

## Conclusion

Griffe suits a Python project that wants API documentation generated from real source or wants a release gate that fails on a changed parameter kind, and it does not suit a team that needs runtime type information or a semantic diff of behaviour. Before adopting it, decide whether you want static extraction or runtime introspection, because Griffe reads the source rather than importing your package, and check the CHANGELOG.md for the 2.4.0 release before pinning a version.

## FAQ

### What does Griffe do and what is it used for?

Griffe extracts the structure of a Python project, its structure and signatures, so that it can be used to generate API documentation or to find breaking changes in an API. It is published under the mkdocstrings organisation and is used as the analysis layer behind documentation tooling.

### How do I install Griffe?

Install it with pip install griffe, or as a standalone tool with uv tool install griffe. The package requires Python 3.11 or newer, and checking packages against PyPI additionally needs the pypi extra installed with pip install griffe[pypi].

### How do I check a Python package for breaking changes?

Run griffe check with a relative path to the package, and add --search src for projects using a src layout. To compare against a published release, install the pypi extra and pass the baseline and candidate versions, for example griffe check package_name -b project-name==2.0 -a project-name==1.0.

### Can I use Griffe from Python instead of the command line?

Yes. griffe.load returns the model of a package, griffe.load_git loads a package at a given git ref, and griffe.find_breaking_changes compares two models and returns the breakages it finds, as shown in the README example.

## Sources

- [License: ISC](https://github.com/mkdocstrings/griffe/blob/main/LICENSE)
- [mkdocstrings/griffe on GitHub](https://github.com/mkdocstrings/griffe)
- [Project website](https://mkdocstrings.github.io/griffe)
- [README](https://github.com/mkdocstrings/griffe/blob/main/README.md)
- [Releases](https://github.com/mkdocstrings/griffe/releases)

---

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