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:
glob | Path.glob | |
---|---|---|
Hidden files | Excludes hidden files by default. From Python 3.11 onwards, the include_hidden keyword can used to include hidden directories. | Includes hidden files by default. |
Iterator | iglob returns an iterator. Under the hood, glob simply converts the iterator to a list. | Path.glob returns an iterator. |
Working directory | glob 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.