Ruff v0.10.0 is available now! Install it from PyPI, or your package manager of choice:

uv pip install --upgrade ruff

As a reminder: Ruff is an extremely fast Python linter and formatter, written in Rust. Ruff can be used to replace Black, Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, and more, all while executing tens or hundreds of times faster than any individual tool.

Migrating to v0.10

As usual for Ruff minor releases, v0.10 contains few breaking changes, and most users should be able to upgrade without significant modifications to their code or configuration. However, there are a few changes worth calling out individually.

Improved Python version detection

This release makes Python version detection more intuitive for projects with a pyproject.toml file.

Ruff uses your Python version to offer more accurate suggestions for lint rules, import sorting, and other formatting decisions — just to name a few.1 If you've been following recent preview changes, we're also working on better detection of version-related syntax errors (such as the use of match before Python 3.10). These rely on your specified Python version to avoid false positives.

In previous versions of Ruff, you could specify your Python version with:

  • The target-version option in a ruff.toml file or the [tool.ruff] section of a pyproject.toml file.
  • The project.requires-python field in a pyproject.toml file with a [tool.ruff] section.

These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, pyproject.toml files without a [tool.ruff] section would be ignored, including the requires-python setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version.

In v0.10, config discovery has been updated to address this issue:

  1. If Ruff finds a ruff.toml file without a target-version, it will check for a pyproject.toml file in the same directory and respect its requires-python version, even if it does not contain a [tool.ruff] section.
  2. If there is no config file (ruff.toml or pyproject.toml with a [tool.ruff] section) in the directory of the file being checked, Ruff will search for the closest pyproject.toml in the parent directories and use its requires-python setting.2

These changes will have no effect if you're already using the target-version option or requires-python in a pyproject.toml with a [tool.ruff] section.3

See the new documentation for more details.

More robust suppression comments

The inline (# noqa) and file-level (# ruff: noqa) suppression comment parsing have been unified and overall made more robust.

In the following situations, a comment will suppress more rules than it used to:

  • Leading comments are now allowed for file-level suppression comments, e.g. # Some context # ruff: noqa. Previously, this noqa was silently ignored.
  • Trailing comments are now allowed for file-level suppression comments, e.g. # ruff: noqa This file is rough!, whereas previously this resulted in an error and the suppression did not take effect.
  • To support the previous change, invalid syntax like ruff: noqa UP035 (missing colon after noqa) will now suppress all rules. This is consistent with the existing inline behavior and is guarded against by blanket-noqa (PGH004).
  • Missing items in lists of rules are now accepted but emit a warning. For example, #noqa: F401,,F841 will suppress F401 and F841 but display a warning.
  • Similarly, missing delimiters in lists of rules are now accepted in both the inline and file-level comments but emit a warning. For example, # ruff: noqa: F401F841 will suppress both F401 and F841 but display a warning.

In the following situations, a comment may suppress fewer rules than it used to:

  • It is now disallowed to have an invalid rule suffix in inline suppression comments, e.g. # noqa: UP035abc. This will log an error and suppress no rules. This matches the current behavior for file-level suppression comments.
  • Whitespace is now allowed between the colon and rule list, e.g. #noqa : F401. This previously suppressed all rules but will now only suppress F401, as was likely intended.

Updated TYPE_CHECKING behavior

This release brings Ruff's treatment of TYPE_CHECKING symbols into alignment with other tools like mypy and pyright.

typing.TYPE_CHECKING is a special constant in the standard library typing module that can be used to avoid expensive imports at runtime but include them for use by type checkers (and linters). This often looks something like this example adapted from the typing documentation:

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import expensive_mod

def fun(arg: expensive_mod.SomeType) -> None: ...

However, in some cases the typing import itself may be undesirable. In the past, Ruff and many type checkers have allowed if statements like if 0: ... or if False: ... in place of if TYPE_CHECKING: ... to avoid the typing import. That has fallen out of favor and been generally replaced by using a local symbol named TYPE_CHECKING instead, for example:

from __future__ import annotations

TYPE_CHECKING = False
if TYPE_CHECKING:
    import expensive_mod

def fun(arg: expensive_mod.SomeType) -> None: ...

This release stabilizes both of these changes in Ruff:

  1. Support for if 0 and if False instead of if TYPE_CHECKING has been removed.
  2. Support for any symbol named TYPE_CHECKING rather than precisely typing.TYPE_CHECKING has been added.

As an added benefit, this change strengthens rules like if-else-block-instead-of-if-exp (SIM108), which previously could not collapse if 0 or if False blocks in case they were actually for TYPE_CHECKING.

Rule stabilizations

The following rules have been stabilized and are no longer in preview:

Other behavior stabilizations

This release also stabilizes some additional behavior, previously only available in preview mode:

  • Options for the flake8-builtins rules with redundant builtins- prefixes have been deprecated. For example, the lint.flake8-builtins.builtins-allowed-modules option is now lint.flake8-builtins.allowed-modules.
  • The flake8-builtins rule stdlib-module-shadowing (A005) now defaults to non-strict module name checking (e.g. a utils/logging.py module will not be flagged as conflicting with the logging builtin module). See the lint.flake8-builtins.strict-checking option to restore the old default.
  • custom-type-var-for-self (PYI019) now uses a more accurate approach for replacing custom TypeVars with Self, can apply to methods without a return annotation, and offers an automatic fix.
  • blanket-noqa (PGH004) now detects file-level suppression comments in addition to inline comments.
  • invalid-envvar-default (PLW1508) has been extended to recognize os.environ.get as well as os.getenv.
  • invalid-first-argument-name-for-class-method (N804) no longer flags __new__ methods, which are technically static methods rather than class methods. bad-staticmethod-argument (PLW0211) now applies to __new__ methods instead.

See the full changelog on GitHub for other, smaller changes.

Rule deprecations

  • suspicious-xmle-tree-usage (S320) has been deprecated because the issues it checks for in the lxml package have been resolved.
  • non-pep604-isinstance (UP038) has been deprecated because using PEP 604 union syntax in isinstance and issubclass calls is not actually a recommended pattern and may also hurt performance.

Thank you!

Thank you to everyone who provided feedback regarding the changes included in Ruff's preview mode, and especially, to our contributors. It's an honor building Ruff with you!


View the full changelog on GitHub.

Read more about Astral — the company behind Ruff.

Thanks to Dylan Wilson, Micha Reiser, Dhruv Manilawala, and Zanie Blue who contributed to this blog post.

Footnotes

  1. See the v0.8 release notes for a more specific list if you're curious!

  2. Ruff only performs this search if the --isolated flag is not used and no target version is passed via the --config or --target-version flags.

  3. These changes also won't affect you if you don't have a project.requires-python setting or a pyproject.toml at all. In those cases, Ruff will continue using its internal default version of Python 3.9 (as of this writing).