Skip to main content

An extremely fast Python linter, written in Rust.

Project description


image image image Actions status

An extremely fast Python linter, written in Rust.

Bar chart with benchmark results

Linting the CPython codebase from scratch.

  • ⚡️ 10-100x faster than existing linters
  • 🐍 Installable via pip
  • 🤝 Python 3.10 compatibility
  • 🛠️ pyproject.toml support
  • 📦 ESLint-inspired cache support
  • 🔧 ESLint-inspired --fix support
  • 👀 TypeScript-inspired --watch support
  • ⚖️ Near-complete parity with the built-in Flake8 rule set

Read the launch blog post.

Installation and usage


Available as ruff on PyPI:

pip install ruff


To run ruff, try any of the following:

ruff path/to/code/to/
ruff path/to/code/
ruff path/to/code/*.py

You can run ruff in --watch mode to automatically re-run on-change:

ruff path/to/code/ --watch

ruff also works with pre-commit:

  - repo:
    rev: v0.0.40
      - id: lint


ruff is configurable both via pyproject.toml and the command line.

For example, you could configure ruff to only enforce a subset of rules with:

line-length = 88
select = [

Alternatively, on the command-line:

ruff path/to/code/ --select F401 --select F403

See ruff --help for more:

ruff: An extremely fast Python linter.

Usage: ruff [OPTIONS] <FILES>...


  -v, --verbose
          Enable verbose logging
  -q, --quiet
          Disable all logging (but still exit with status code "1" upon detecting errors)
  -e, --exit-zero
          Exit with status code "0", even upon detecting errors
  -w, --watch
          Run in watch mode by re-running whenever files change
  -f, --fix
          Attempt to automatically fix lint errors
  -n, --no-cache
          Disable cache reads
      --select <SELECT>
          List of error codes to enable
      --extend-select <EXTEND_SELECT>
          Like --select, but adds additional error codes on top of the selected ones
      --ignore <IGNORE>
          List of error codes to ignore
      --extend-ignore <EXTEND_IGNORE>
          Like --ignore, but adds additional error codes on top of the ignored ones
      --exclude <EXCLUDE>
          List of paths, used to exclude files and/or directories from checks
      --extend-exclude <EXTEND_EXCLUDE>
          Like --exclude, but adds additional files and directories on top of the excluded ones
      --per-file-ignores <PER_FILE_IGNORES>
          List of mappings from file pattern to code to exclude
      --format <FORMAT>
          Output serialization format for error messages [default: text] [possible values: text, json]
          See the files ruff will be run against with the current settings
          See ruff's settings
          Enable automatic additions of noqa directives to failing lines
      --dummy-variable-rgx <DUMMY_VARIABLE_RGX>
          Regular expression matching the name of dummy variables
  -h, --help
          Print help information
  -V, --version
          Print version information

Excluding files

Exclusions are based on globs, and can be either:

  • Single-path patterns, like .mypy_cache (to exclude any directory named .mypy_cache in the tree), (to exclude any file named, or foo_*.py (to exclude any file matching foo_*.py ).
  • Relative patterns, like directory/ (to exclude that specific file) or directory/*.py (to exclude any Python files in directory). Note that these paths are relative to the project root (e.g., the directory containing your pyproject.toml).

Ignoring errors

To omit a lint check entirely, add it to the "ignore" list via --ignore or --extend-ignore, either on the command-line or in your project.toml file.

To ignore an error in-line, ruff uses a noqa system similar to Flake8. To ignore an individual error, add # noqa: {code} to the end of the line, like so:

# Ignore F841.
x = 1  # noqa: F841

# Ignore E741 and F841.
i = 1  # noqa: E741, F841

# Ignore _all_ errors.
x = 1  # noqa

Note that, for multi-line strings, the noqa directive should come at the end of the string, and will apply to the entire body, like so:

"""Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""  # noqa: E501

ruff supports several (experimental) workflows to aid in noqa management.

First, ruff provides a special error code, M001, to enforce that your noqa directives are "valid", in that the errors they say they ignore are actually being triggered on that line (and thus suppressed). You can run ruff /path/to/ --extend-select M001 to flag unused noqa directives.

Second, ruff can automatically remove unused noqa directives via its autofix functionality. You can run ruff /path/to/ --extend-select M001 --fix to automatically remove unused noqa directives.

Third, ruff can automatically add noqa directives to all failing lines. This is useful when migrating a new codebase to ruff. You can run ruff /path/to/ --add-noqa to automatically add noqa directives to all failing lines, with the appropriate error codes.

Compatibility with Black

ruff is compatible with Black out-of-the-box, as long as the line-length setting is consistent between the two.

As a project, ruff is designed to be used alongside Black and, as such, will defer implementing stylistic lint rules that are obviated by autoformatting.

Parity with Flake8

ruff's goal is to achieve feature-parity with Flake8 when used (1) without plugins, (2) alongside Black, and (3) on Python 3 code.

Under those conditions, ruff implements 44 out of 60 rules. (ruff is missing: 14 rules related to string .format calls, 1 rule related to docstring parsing, and 1 rule related to redefined variables.)

Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:

  1. Flake8 has a plugin architecture and supports writing custom lint rules.
  2. ruff does not yet support parenthesized context managers.


Code Name Message
E402 ModuleImportNotAtTopOfFile Module level import not at top of file
E501 LineTooLong Line too long (89 > 88 characters)
E711 NoneComparison Comparison to None should be cond is None
E712 TrueFalseComparison Comparison to True should be cond is True
E713 NotInTest Test for membership should be not in
E714 NotIsTest Test for object identity should be is not
E721 TypeComparison do not compare types, use isinstance()
E722 DoNotUseBareExcept Do not use bare except
E731 DoNotAssignLambda Do not assign a lambda expression, use a def
E741 AmbiguousVariableName ambiguous variable name '...'
E742 AmbiguousClassName ambiguous class name '...'
E743 AmbiguousFunctionName ambiguous function name '...'
E902 IOError ...
E999 SyntaxError SyntaxError: ...
F401 UnusedImport ... imported but unused
F402 ImportShadowedByLoopVar import '...' from line 1 shadowed by loop variable
F403 ImportStarUsed from ... import * used; unable to detect undefined names
F404 LateFutureImport from future imports must occur at the beginning of the file
F405 ImportStarUsage '...' may be undefined, or defined from star imports: ...
F406 ImportStarNotPermitted from ... import * only allowed at module level
F407 FutureFeatureNotDefined future feature '...' is not defined
F541 FStringMissingPlaceholders f-string without any placeholders
F601 MultiValueRepeatedKeyLiteral Dictionary key literal repeated
F602 MultiValueRepeatedKeyVariable Dictionary key ... repeated
F621 TooManyExpressionsInStarredAssignment too many expressions in star-unpacking assignment
F622 TwoStarredExpressions two starred expressions in assignment
F631 AssertTuple Assert test is a non-empty tuple, which is always True
F632 IsLiteral use ==/!= to compare constant literals
F633 InvalidPrintSyntax use of >> is invalid with print function
F634 IfTuple If test is a tuple, which is always True
F701 BreakOutsideLoop break outside loop
F702 ContinueOutsideLoop continue not properly in loop
F704 YieldOutsideFunction a yield or yield from statement outside of a function/method
F706 ReturnOutsideFunction a return statement outside of a function/method
F707 DefaultExceptNotLast an except: block as not the last exception handler
F722 ForwardAnnotationSyntaxError syntax error in forward annotation '...'
F821 UndefinedName Undefined name ...
F822 UndefinedExport Undefined name ... in __all__
F823 UndefinedLocal Local variable ... referenced before assignment
F831 DuplicateArgumentName Duplicate argument name in function definition
F841 UnusedVariable Local variable ... is assigned to but never used
F901 RaiseNotImplemented raise NotImplemented should be raise NotImplementedError
R001 UselessObjectInheritance Class ... inherits from object
R002 NoAssertEquals assertEquals is deprecated, use assertEqual instead
M001 UnusedNOQA Unused noqa directive


ruff is written in Rust (1.63.0). You'll need to install the Rust toolchain for development.

Assuming you have cargo installed, you can run:

cargo run resources/test/fixtures
cargo fmt
cargo clippy
cargo test


ruff is distributed on PyPI, and published via maturin.

See: .github/workflows/release.yaml.


First, clone CPython. It's a large and diverse Python codebase, which makes it a good target for benchmarking.

git clone --branch 3.10 resources/test/cpython

Add this pyproject.toml to the CPython directory:

line-length = 88
extend-exclude = [

Next, to benchmark the release build:

cargo build --release

hyperfine --ignore-failure --warmup 10 --runs 100 \
  "./target/release/ruff ./resources/test/cpython/ --no-cache" \
  "./target/release/ruff ./resources/test/cpython/"

Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
  Time (mean ± σ):     297.4 ms ±   4.9 ms    [User: 2460.0 ms, System: 67.2 ms]
  Range (min  max):   287.7 ms  312.1 ms    100 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: ./target/release/ruff ./resources/test/cpython/
  Time (mean ± σ):      79.6 ms ±   7.3 ms    [User: 59.7 ms, System: 356.1 ms]
  Range (min  max):    62.4 ms  111.2 ms    100 runs

  Warning: Ignoring non-zero exit code.

To benchmark against the ecosystem's existing tools:

hyperfine --ignore-failure --warmup 5 \
  "./target/release/ruff ./resources/test/cpython/ --no-cache" \
  "pylint --recursive=y resources/test/cpython/" \
  "pyflakes resources/test/cpython" \
  "autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
  "pycodestyle resources/test/cpython" \
  "flake8 resources/test/cpython" \
  "python -m scripts.run_flake8 resources/test/cpython"

In order, these evaluate:

  • ruff
  • Pylint
  • PyFlakes
  • autoflake
  • pycodestyle
  • Flake8
  • Flake8, with a hack to enable multiprocessing on macOS

(You can poetry install from ./scripts to create a working environment for the above.)

Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
  Time (mean ± σ):     297.9 ms ±   7.0 ms    [User: 2436.6 ms, System: 65.9 ms]
  Range (min  max):   289.9 ms  314.6 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: pylint --recursive=y resources/test/cpython/
  Time (mean ± σ):     37.634 s ±  0.225 s    [User: 36.728 s, System: 0.853 s]
  Range (min  max):   37.201 s  38.106 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 3: pyflakes resources/test/cpython
  Time (mean ± σ):     40.950 s ±  0.449 s    [User: 40.688 s, System: 0.229 s]
  Range (min  max):   40.348 s  41.671 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
  Time (mean ± σ):     11.562 s ±  0.160 s    [User: 107.022 s, System: 1.143 s]
  Range (min  max):   11.417 s  11.917 s    10 runs

Benchmark 5: pycodestyle resources/test/cpython
  Time (mean ± σ):     67.428 s ±  0.985 s    [User: 67.199 s, System: 0.203 s]
  Range (min  max):   65.313 s  68.496 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 6: flake8 resources/test/cpython
  Time (mean ± σ):     116.099 s ±  1.178 s    [User: 115.217 s, System: 0.845 s]
  Range (min  max):   114.180 s  117.724 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
  Time (mean ± σ):     20.477 s ±  0.349 s    [User: 142.372 s, System: 1.504 s]
  Range (min  max):   20.107 s  21.183 s    10 runs

  './target/release/ruff ./resources/test/cpython/ --no-cache' ran
   38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
   68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
  126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
  137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
  226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
  389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'



Project details

Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ruff-0.0.47.tar.gz (115.5 kB view hashes)

Uploaded Source

Built Distributions

ruff-0.0.47-py3-none-win_amd64.whl (2.5 MB view hashes)

Uploaded Python 3 Windows x86-64

ruff-0.0.47-py3-none-win32.whl (2.4 MB view hashes)

Uploaded Python 3 Windows x86

ruff-0.0.47-py3-none-musllinux_1_2_x86_64.whl (2.8 MB view hashes)

Uploaded Python 3 musllinux: musl 1.2+ x86-64

ruff-0.0.47-py3-none-musllinux_1_2_i686.whl (2.7 MB view hashes)

Uploaded Python 3 musllinux: musl 1.2+ i686

ruff-0.0.47-py3-none-musllinux_1_2_armv7l.whl (2.4 MB view hashes)

Uploaded Python 3 musllinux: musl 1.2+ ARMv7l

ruff-0.0.47-py3-none-musllinux_1_2_aarch64.whl (2.6 MB view hashes)

Uploaded Python 3 musllinux: musl 1.2+ ARM64

ruff-0.0.47-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.7 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ x86-64

ruff-0.0.47-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl (2.2 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ s390x

ruff-0.0.47-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl (2.1 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ ppc64le

ruff-0.0.47-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl (2.1 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ ppc64

ruff-0.0.47-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl (2.8 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ i686

ruff-0.0.47-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl (1.8 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ ARMv7l

ruff-0.0.47-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.6 MB view hashes)

Uploaded Python 3 manylinux: glibc 2.17+ ARM64

ruff-0.0.47-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl (5.2 MB view hashes)

Uploaded Python 3 macOS 10.9+ universal2 (ARM64, x86-64) macOS 10.9+ x86-64 macOS 11.0+ ARM64

ruff-0.0.47-py3-none-macosx_10_7_x86_64.whl (2.7 MB view hashes)

Uploaded Python 3 macOS 10.7+ x86-64

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page