# Ruff's real edges: a 0.16.x cadence, a two-platform container, and a requires-python of 3.7

> astral-sh/ruff is a MIT licensed Python linter and formatter written in Rust, claiming 10 to 100 times the speed of Flake8 and Black with over 900 built-in rules. The details that decide whether it fits your build are elsewhere: a 0.x release train shipping weekly, a requires-python floor of 3.7, a container that builds for two architectures and fails on the rest, and a pip install that is really a native binary.

**astral-sh/ruff** — Ruff is a Python linter and code formatter written in Rust, bundling over 900 rules with Flake8, isort, and Black compatibility plus automatic error fixing.

- Repository: https://github.com/astral-sh/ruff
- Website: https://docs.astral.sh/ruff
- Stars: 49,846 · Forks: 2,454
- Language: Rust
- License: MIT
- Published: 2026-08-08 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/astral-sh-ruff

## requires-python is 3.7 while the classifiers stop at 3.14 and the overview claims 3.15

The packaging metadata and the marketing line are describing different things. The project file sets requires-python to >=3.7 and lists classifiers from 3.7 through 3.14, with 3 :: Only, alongside a Development Status of 5 - Production/Stable, an OS Independent environment, and Rust itself as a classifier. The overview bullet list says Python 3.15 compatibility. So the highest interpreter named in the metadata is 3.14 while the highest one supported is 3.15, and a resolver on 3.15 is relying on that sentence rather than on the metadata. The 3.7 floor is the other half of the problem. A wheel that declares support for 3.7 promises to run on an interpreter long past its own end of life, which is a maintenance burden the project has chosen to keep rather than a guarantee you can lean on. For a team standardising on a recent interpreter, the floor costs nothing; for a team that wants the metadata to describe its actual support window, the classifier list is where the gap shows.

## A 0.x train shipping every few days keeps a BREAKING_CHANGES.md at the root

The version numbers explain the file layout. The three most recent releases are 0.16.9 on 2026-09-24, 0.16.8 on 2026-09-16, and 0.16.7 on 2026-09-10, which is three releases in a fortnight on a 0.x line, and the top level carries both a BREAKING_CHANGES.md and a changelogs/ directory beside the main CHANGELOG.md. Breaking changes are therefore not hypothetical here, they are documented in a file you can read before upgrading. The consequence is for automation rather than for humans. A constraint that floats, whether in a pre-commit configuration or a CI action, will change the rules your build enforces on a schedule you did not choose, and a formatter change can produce a diff in files nobody touched since the last run. Pinning the version is the answer the project's own examples already model, since the first thing the install section shows is an invocation with an explicit version attached.

## The Dockerfile builds for two architectures and exits on every other platform

The container build is a cross-compilation script with an explicit allowlist. It installs build-essential, curl, and python3-venv on an ubuntu base, then pip installs cargo-zigbuild as the cross compiling linker, and switches on TARGETPLATFORM in a shell case statement: linux/arm64 maps to aarch64-unknown-linux-musl, linux/amd64 maps to x86_64-unknown-linux-musl, and every other value hits an exit 1. So the file supports two architectures and refuses the rest, including 32-bit ARM, s390x, and any Windows container platform. The build then copies rust-toolchain.toml, installs the pinned toolchain with a minimal profile and no default, adds the musl target, and runs cargo zigbuild for the ruff binary in release mode, with a comment reminding the reader to update rustup whenever the rust version is bumped. If your CI runs on a platform outside that list, this Dockerfile is not the route: the standalone installers or the PyPI wheel are.

## The final image is FROM scratch with one binary, and it is not stripped the way the wheel is

Read the last four lines of the build and the runtime shape is fixed. The final stage is FROM scratch, it copies only the built binary to /ruff, it sets WORKDIR to /io, and it declares the entrypoint as that binary. Nothing else is in the image, which means no shell, no package manager, and no certificate bundle for anything the binary might need to reach over TLS, so debugging inside the container means reproducing outside it. There is also an asymmetry worth knowing. The project file sets strip = true under the maturin configuration, so the pip wheel is stripped, while the Dockerfile has the strip command commented out under a note about optimising binary size with a version that also works when cross compiling. The consequence is that the container image ships a larger binary than the wheel you would install from PyPI, and a size comparison between the two artefacts is comparing two different things.

## Five install paths, and only two of them accept a version

The install section is a menu, and reading it as a menu shows where pinning is possible. The first route runs the tool without installing it, with a version attached:

```shell
uvx ruff@0.16.9 check   # Lint all files in the current directory.
uvx ruff@0.16.9 format  # Format all files in the current directory.
```

Then there is uv with `uv tool install ruff@latest` or `uv add --dev ruff`, pip with `pip install ruff`, pipx with `pipx install ruff`, and since version 0.5.0 the standalone installers, which are a curl of an install script piped to sh on macOS and Linux and a PowerShell one-liner on Windows, both of which also accept a version in the URL. Homebrew and Conda are offered as well. Of these, the uvx invocation and the versioned installer URLs take an explicit number, while uv add, pip, pipx, Homebrew, and Conda all resolve to whatever the channel currently serves. For a local machine that is the right default, and for a build that has to be reproducible it is the difference between a pipeline that lints the same code tomorrow and one that lints whatever shipped this week.

## The 10 to 100 times claim and the testimonials measure different baselines

The headline figure is a range: 10 to 100 times faster than existing linters such as Flake8 and formatters such as Black. The testimonials behind it quote much larger numbers, and the gap is instructive rather than suspicious. One compares Ruff against pylint on dagster itself at 250k lines, where pylint takes about 2.5 minutes parallelised across 4 cores on an M1 and Ruff against the entire codebase takes 0.4 seconds. Another compares against flake8, about 20 seconds down to roughly 0.2 seconds, which is 150 to 200 times. So the baseline tool, the codebase, and the work being measured differ between claims, and the ratio you get is a property of your repository rather than of Ruff. Two of the five testimonials make a related point that is worth taking seriously: the author of FastAPI and the creator of isort both describe adding deliberate errors to confirm the tool was actually running.

## The Python package is a native binary built by maturin, not a Python library

What pip installs is a launcher for a compiled program. The build backend is maturin, the maturin configuration sets bindings to bin, points the manifest at crates/ruff/Cargo.toml, sets python-source to python, strips the binary, and excludes the linter test fixtures and the rule snapshots from the distribution. The Rust side is a Cargo workspace whose members are every crate under crates/, on edition 2024 with rust-version 1.96, and whose internal crates are declared as path dependencies alongside an external char_str at 0.0.4. Two consequences. Environments that forbid binary wheels, such as some locked-down build pipelines or slim distroless images, will not take the PyPI route and need a different plan. And building from source is not casual, since it needs a pinned toolchain from rust-toolchain.toml and a recent Rust, which is why the Dockerfile copies that file in before installing anything.

## The repository carries its own agent instructions, a fuzz harness, and a blame ignore list

Several of the top level entries are about how the project is worked on rather than about linting, and none of them appears in the overview. There is an agents/ directory alongside .agents/, .claude/, AGENTS.md, and CLAUDE.md, so conventions are partly expressed as instructions for AI coding agents. There is a fuzz/ directory, which matters more than it looks, because a linter parses code it did not write and a parser is exactly the component that should be fuzzed. There is a .git-blame-ignore-revs file, the project's own answer to the problem a formatter creates when one commit rewrites every line. And there are two JSON schemas at the root, ruff.schema.json and ty.schema.json, where the second belongs to ty, the Astral type checker that the overview only mentions as a sibling project of uv. Add _typos.toml, .markdownlint.yaml, clippy.toml, rustfmt.toml, and a mkdocs pair for the documentation, and the picture is a project whose tooling dogfoods itself.

## Conclusion

Ruff is a good default for a Python project that wants one tool covering lint, format, and import sorting, and the speed difference against Flake8 plus Black plus isort is large enough to change how often you run the linter, including on every commit. Two habits make that work. Pin the version in CI, because a 0.x train released 0.16.7, 0.16.8, and 0.16.9 in a fortnight keeps a BREAKING_CHANGES.md at the root, and a floating constraint means your pipeline changes rules without you editing anything. And do not treat the headline multiples as a forecast, since the 10 to 100 times figure is measured against Flake8 and Black while the testimonials measure against pylint on a 250,000 line module, and two of those quotes are about being unable to tell whether the tool had run at all. Before containerising, check your platform against the Dockerfile, which builds for linux/amd64 and linux/arm64 and exits on anything else.

## FAQ

### Which linter is better, Pylint or Ruff?

The project does not compare them as linters. Its replacement list names Flake8 plus dozens of plugins, Black, isort, pydocstyle, pyupgrade, autoflake, and more, and Pylint appears only in a pyproject keyword and in a testimonial that measures Ruff against pylint on a 250k line module. A FAQ page covers how the linter compares to Flake8.

### Which formatter is better, Ruff or Black?

The project claims drop-in parity with Black and links a FAQ page explaining how the formatter compares to Black, and the formatter is the format subcommand, for example `ruff format` for all files in the current directory. The overview states the parity claim without publishing a catalogue of where the output differs.

### How to ignore Ruff error?

The overview does not document the suppression syntax. What it does give you is over 900 built-in rules with native re-implementations of popular Flake8 plugins, pyproject.toml support, hierarchical and cascading configuration for monorepos, built-in caching so unchanged files are not re-analyzed, and fix support for automatic correction, with the full documentation at docs.astral.sh/ruff.

### Can Ruff replace pylint?

Pylint is not in the project's stated replacement list, which covers Flake8 plus dozens of plugins, Black, isort, pydocstyle, pyupgrade, autoflake, and more, so the documented claim is about those tools rather than about Pylint. Pylint is named as a keyword in the project file and in one testimonial, where a whole 250k line codebase takes 0.4 seconds against 2.5 minutes for pylint.

## Sources

- [Official documentation](https://docs.astral.sh/ruff)
- [Official README](https://github.com/astral-sh/ruff#readme)
- [Project repository](https://github.com/astral-sh/ruff)
- [Release notes](https://github.com/astral-sh/ruff/releases)

---

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