Ruff v0.0.281 is out now. Install it from PyPI, or your package manager of choice:

pip install --upgrade ruff

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

View the full changelog on GitHub, or read on for the highlights.

Ruff's lexer is 2-3x faster

Ruff's lexer is now 2-3x faster than in previous releases:

group                       v0.0.280                               v0.0.281
-----                       --------                               --------
lexer/large/dataset.py      2.18    665.9±5.64µs    61.1 MB/sec    1.00    304.9±3.79µs   133.4 MB/sec
lexer/numpy/ctypeslib.py    2.39    154.4±0.84µs   107.8 MB/sec    1.00     64.5±0.61µs   258.1 MB/sec
lexer/numpy/globals.py      2.89     18.1±0.14µs   163.3 MB/sec    1.00      6.3±0.06µs   471.8 MB/sec
lexer/pydantic/types.py     2.57    326.4±2.23µs    78.1 MB/sec    1.00    127.2±0.71µs   200.5 MB/sec

The lexer is responsible for tokenizing Python source code into a stream of tokens, which are then used by the parser to build an abstract syntax tree (AST). The lexer is the first step in Ruff's analysis pipeline, and is run on every file that Ruff analyzes.

Improving lexer performance not only improves linter performance, but will also improve the performance of future tools that leverage Ruff's lexer, like Ruff's formatter.

The new lexer leverages more cache-friendly data structures, performs fewer allocations, and includes optimizations for ASCII-only source code. See #38 for more details.

End-of-line # ruff: noqa comments are now ignored

Ruff allows lint rules to be disabled across an entire file via # ruff: noqa suppression comments. For example, the following will ignore all lint errors across the file:

# ruff: noqa
import os
import sys

Similarly, the following will ignore all unused import (F401) errors across the file:

# ruff: noqa: F401
import os
import sys

Historically, Ruff respected these suppression comments even when they appeared at the end of a line, as in:

import os  # ruff: noqa: F401
import sys

Such end-of-line comments are likely a mistake (instead, use # noqa: F401 to suppress an error on a single line). Ruff will now warn and ignore these end-of-line, file-level suppression comments.

New rule: unused-private-type-var (PYI018)

What does it do?

Checks for the presence of unused private TypeVar declarations.

Why does it matter?

A private TypeVar that is defined but not used is likely a mistake, and should either be used, made public, or removed to avoid confusion.

For example, given the following snippet:

import typing

_T = typing.TypeVar("_T")

The TypeVar is not used anywhere, and so should either be used, made public, or removed.

This rule is derived from flake8-pyi.

Contributed by @LaBatata101.

New rule: unused-private-protocol (PYI046)

What does it do?

Checks for the presence of unused private typing.Protocol definitions.

Why does it matter?

A private typing.Protocol that is defined but not used is likely a mistake, and should either be used, made public, or removed to avoid confusion.

For example, given the following snippet:

import typing


class _PrivateProtocol(typing.Protocol):
    field: int

The _PrivateProtocol class is not used anywhere, and so should either be used, made public, or removed.

This rule is derived from flake8-pyi.

Contributed by @LaBatata101.

New rule: unused-private-type-alias (PYI047)

What does it do?

Checks for the presence of unused private typing.TypeAlias definitions.

Why does it matter?

A private typing.TypeAlias that is defined but not used is likely a mistake, and should either be used, made public, or removed to avoid confusion.

For example, given the following snippet:

import typing

_UnusedTypeAlias: typing.TypeAlias = int

The type alias is not used anywhere, and so should either be used, made public, or removed.

This rule is derived from flake8-pyi.

Contributed by @LaBatata101.

New rule: unused-private-typed-dict (PYI049)

What does it do?

Checks for the presence of unused private typing.TypedDict definitions.

Why does it matter?

A private typing.TypedDict that is defined but not used is likely a mistake, and should either be used, made public, or removed to avoid confusion.

For example, given the following snippet:

import typing


class _UnusedPrivateTypedDict(typing.TypedDict):
    field: list[int]

The __UnusedPrivateTypedDict class is not used anywhere, and so should either be used, made public, or removed.

This rule is derived from flake8-pyi.

Contributed by @LaBatata101.

New rule: unsupported-method-call-on-all (PYI056)

What does it do?

Checks that append, extend and remove methods are not called on __all__.

Why does it matter?

Different type checkers have varying levels of support for calling these methods on __all__. Instead, use the += operator to add items to __all__, which is known to be supported by all major type checkers.

For example, given the following snippet:

__all__ = ["A"]
__all__.append("B")

Instead, use an augmented assignment statement to add items to __all__:

__all__ = ["A"]
__all__ += ["B"]

This rule is derived from flake8-pyi.

Contributed by @LaBatata101.

New rule: self-assigning-variable (PLW0127)

What does it do?

Checks for self-assignment of variables.

Why does it matter?

Self-assignment of variables is redundant and likely a mistake.

For example, given the following snippet:

country = "Poland"
country = country

Instead, use the variable directly:

country = "Poland"

This rule is derived from Pylint.

Contributed by @tjkuson.

New rule: subprocess-popen-preexec-fn (PLW1509)

What does it do?

Checks for uses of subprocess.Popen with a preexec_fn argument.

Why does it matter?

The preexec_fn argument is unsafe within threads as it can lead to deadlocks. Furthermore, preexec_fn is targeted for deprecation.

Instead, consider using task-specific arguments such as env, start_new_session, and process_group. These are not prone to deadlocks and are more explicit.

For example, given the following snippet:

import os, subprocess

subprocess.Popen(foo, preexec_fn=os.setsid)
subprocess.Popen(bar, preexec_fn=os.setpgid(0, 0))

Instead, use arguments like start_new_session and process_group:

import subprocess

subprocess.Popen(foo, start_new_session=True)
subprocess.Popen(bar, process_group=0)  # Introduced in Python 3.11

This rule is derived from Pylint.

Contributed by @tjkuson.

New rule: os-sep-split (PTH206)

What does it do?

Checks for uses of .split(os.sep)

Why does it matter?

The pathlib module in the standard library should be used for path manipulation. It provides a high-level API with the functionality needed for common operations on Path objects.

For example, given the following snippet:

If not all parts of the path are needed, then the name and parent attributes of the Path object should be used. Otherwise, the parts attribute can be used as shown in the last example.

import os

"path/to/file_name.txt".split(os.sep)[-1]

"path/to/file_name.txt".split(os.sep)[-2]

if any(part in blocklist for part in "my/file/path".split(os.sep)):
    ...

Instead, use the Path object:

from pathlib import Path

Path("path/to/file_name.txt").name

Path("path/to/file_name.txt").parent.name

if any(part in blocklist for part in Path("my/file/path").parts):
    ...

Contributed by @sbrugman.

New rule: glob (PTH207)

What does it do?

Checks for the use of glob and iglob.

Why does it matter?

pathlib offers a high-level API for path manipulation, as compared to the lower-level API offered by os and glob.

When possible, using Path object methods such as Path.glob() can improve readability over their low-level counterparts (e.g., glob.glob()).

Note that glob.glob and Path.glob are not exact equivalents:

globPath.glob
Hidden filesExcludes hidden files by default. From Python 3.11 onwards, the include_hidden keyword can used to include hidden directories.Includes hidden files by default.
Iteratoriglob returns an iterator. Under the hood, glob simply converts the iterator to a list.Path.glob returns an iterator.
Working directoryglob takes a root_dir keyword to set the current working directory.Path.rglob can be used to return the relative path.
Globstar (**)glob requires the recursive flag to be set to True for the ** pattern to match any files and zero or more directories, subdirectories, and symbolic links.The ** pattern in Path.glob means "this directory and all subdirectories, recursively". In other words, it enables recursive globbing.

For example, given the following snippet:

import glob
import os

glob.glob(os.path.join(path, "requirements*.txt"))

Instead, use the Path object:

from pathlib import Path

Path(path).glob("requirements*.txt")

Contributed by @sbrugman.