Ruff v0.0.276 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.
Jupyter Notebook support #
Ruff now ships with experimental support for linting Jupyter Notebooks:
# Run Ruff over `Notebook.ipynb`.
ruff check Notebook.ipynb
# Re-run Ruff on-save.
ruff check Notebook.ipynb --watch
# Fix any fixable errors in `Notebook.ipynb`.
ruff check Notebook.ipynb --fix
To opt-in to linting Jupyter Notebook files, add the *.ipynb
pattern to your include
setting, like so:
[tool.ruff]
# Allow Ruff to discover `*.ipynb` files.
include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"]
This will prompt Ruff to discover Jupyter Notebook files in any specified directories, and lint them accordingly.
Alternatively, you can always pass a Notebook file to ruff
directly. For example,
ruff check /path/to/notebook.ipynb
will always lint notebook.ipynb
.
You can disable specific rules in Jupyter Notebooks using Ruff's per-file-ignores
setting. For example, it's common to allow imports to appear in any cell, rather than confining them
to the top of the Notebook:
[tool.ruff]
# Allow imports to appear anywhere in Jupyter Notebooks.
per-file-ignores = { "*.ipynb" = ["E402"] }
Jupyter Notebook support is currently opt-in and experimental, and comes with a few known limitations which will be ironed out in subsequent releases (most notably: lack of support for cells with mixed Jupyter Magics and Python code).
We'd love your help testing it out. Have feedback? Run into issues? Big or small, don't hesitate to let us know.
First-class import resolution #
Ruff now includes a first-class import resolver, based on Pyright's import resolver, with support for resolving imports from namespace packages, type stubs, and more.
In future releases, the resolver will allow us to remove a variety of user-provided settings (e.g.,
namespace-packages
), resolve references
across files, detect unused dependencies, and more.
New rule: unnecessary-list-cast
(PERF101
) #
What does it do? #
Checks for explicit casts to list
on for-loop iterables.
Why does it matter? #
Using a list()
call to eagerly iterate over an already-iterable type
(like a tuple, list, or set) is inefficient, as it forces Python to create
a new list unnecessarily.
Removing the list()
call will not change the behavior of the code, but
may improve performance.
Note that, as with all perflint rules, this is only intended as a micro-optimization, and will have a negligible impact on performance in most cases.
For example, given the following snippet:
items = (1, 2, 3)
for i in list(items):
print(i)
Instead, use the iterable directly:
items = (1, 2, 3)
for i in items:
print(i)
New rule: try-except-in-loop
(PERF203
) #
What does it do? #
Checks for uses of except handling via try
-except
within for
and
while
loops.
Why does it matter? #
Exception handling via try
-except
blocks incurs some performance
overhead, regardless of whether an exception is raised.
When possible, refactor your code to put the entire loop into the
try
-except
block, rather than wrapping each iteration in a separate
try
-except
block.
This rule is only enforced for Python versions prior to 3.11, which introduced "zero cost" exception handling.
Note that, as with all perflint rules, this is only intended as a micro-optimization, and will have a negligible impact on performance in most cases.
For example, given the following snippet:
for i in range(10):
try:
print(i * i)
except:
break
Instead, move the entire loop into the try
-except
block:
try:
for i in range(10):
print(i * i)
except:
pass
New rule: manual-list-comprehension
(PERF401
) #
What does it do? #
Checks for for
loops that can be replaced by a list comprehension.
Why does it matter? #
When creating a transformed list from an existing list using a for-loop, prefer a list comprehension. List comprehensions are more readable and more performant.
Using the below as an example, the list comprehension is ~10% faster on Python 3.11, and ~25% faster on Python 3.10.
Note that, as with all perflint
rules, this is only intended as a
micro-optimization, and will have a negligible impact on performance in
most cases.
For example, given the following snippet:
original = list(range(10000))
filtered = []
for i in original:
if i % 2:
filtered.append(i)
Use instead:
original = list(range(10000))
filtered = [x for x in original if x % 2]
If you're instead appending to an existing list, use the extend
method:
original = list(range(10000))
filtered.extend(x for x in original if x % 2)
New rule: manual-list-copy
(PERF402
) #
What does it do? #
Checks for for
loops that can be replaced by a making a copy of a list.
Why does it matter? #
When creating a copy of an existing list using a for-loop, prefer
list
or list.copy
instead. Making a direct copy is more readable and
more performant.
Using the below as an example, the list
-based copy is ~2x faster on
Python 3.11.
Note that, as with all perflint
rules, this is only intended as a
micro-optimization, and will have a negligible impact on performance in
most cases.
For example, given the following snippet:
original = list(range(10000))
filtered = []
for i in original:
filtered.append(i)
Use instead:
original = list(range(10000))
filtered = list(original)
New rule: numpy-deprecated-function
(NPY003
) #
What does it do? #
Checks for uses of deprecated NumPy functions.
Why does it matter? #
NumPy 1.25.0 finalized a variety of deprecations that were introduced in previous releases.
When NumPy functions are deprecated, they are usually replaced with newer, more efficient versions, or with functions that are more consistent with the rest of the NumPy API. Prefer newer APIs over deprecated ones.
For example, given the following snippet:
import numpy as np
np.alltrue([True, False])
Use instead:
import numpy as np
np.all([True, False])
New rule: single-string-slots
(PLC0205
) #
What does it do? #
Checks for single strings assigned to __slots__
.
Why does it matter? #
Any string iterable may be assigned to __slots__
(most commonly, a
tuple
of strings). If a string is assigned to __slots__
, it is
interpreted as a single attribute name, rather than an iterable of attribute
names. This can cause confusion, as users that iterate over the __slots__
value may expect to iterate over a sequence of attributes, but would instead
iterate over the characters of the string.
To use a single string attribute in __slots__
, wrap the string in an
iterable container type, like a tuple
.
For example, given the following snippet:
class Person:
__slots__: str = "name"
def __init__(self, name: str) -> None:
self.name = name
Instead, wrap the string in a tuple
:
class Person:
__slots__: tuple[str, ...] = ("name",)
def __init__(self, name: str) -> None:
self.name = name
New rule: complex-if-statement-in-stub
(PYI002
) #
What does it do? #
Checks for if
statements with complex conditionals in stubs.
Why does it matter? #
Stub files support simple conditionals to test for differences in Python versions and platforms. However, type checkers only understand a limited subset of these conditionals; complex conditionals may result in false positives or false negatives.
For example, given the following snippet:
import sys
if (2, 7) < sys.version_info < (3, 5):
...
Use instead:
import sys
if sys.version_info < (3, 5):
...
New rule: unrecognized-version-info-check
(PYI003
) #
What does it do? #
Checks for problematic sys.version_info
-related conditions in stubs.
Why does it matter? #
Stub files support simple conditionals to test for differences in Python
versions using sys.version_info
. However, there are a number of common
mistakes involving sys.version_info
comparisons that should be avoided.
For example, comparing against a string can lead to unexpected behavior.
For example, given the following snippet:
import sys
if sys.version_info[0] == "2":
...
Use instead:
import sys
if sys.version_info[0] == 2:
...
New rule: patch-version-comparison
(PYI004
) #
What does it do? #
Checks for Python version comparisons in stubs that compare against patch versions (e.g., Python 3.8.3) instead of major and minor versions (e.g., Python 3.8).
Why does it matter? #
Stub files support simple conditionals to test for differences in Python versions and platforms. However, type checkers only understand a limited subset of these conditionals. In particular, type checkers don't support patch versions (e.g., Python 3.8.3), only major and minor versions (e.g., Python 3.8). Therefore, version checks in stubs should only use the major and minor versions.
For example, given the following snippet:
import sys
if sys.version_info >= (3, 4, 3):
...
Use instead:
import sys
if sys.version_info >= (3, 4):
...
New rule: wrong-tuple-length-version-comparison
(PYI005
) #
What does it do? #
Checks for Python version comparisons that compare against a tuple of the wrong length.
Why does it matter? #
Stub files support simple conditionals to test for differences in Python
versions and platforms. When comparing against sys.version_info
, avoid
comparing against tuples of the wrong length, which can lead to unexpected
behavior.
For example, given the following snippet:
import sys
if sys.version_info[:2] == (3,):
...
Use instead:
import sys
if sys.version_info[0] == 3:
...
Bug fixes #
- Support
pydantic.BaseSettings
inmutable-class-default
by @charliermarsh in #5312 - Allow
__slots__
assignments inmutable-class-default
by @charliermarsh in #5314 - Avoid syntax errors when removing f-string prefixes by @charliermarsh in #5319
- Ignore unpacking in
iteration-over-set
by @charliermarsh in #5392 - Replace same length equal line with dash line in D407 by @dhruvmanila in #5383
- Exclude docstrings from PYI053 by @intgr in #5405
- Use "manual" fixability for E731 in shadowed context by @charliermarsh in #5430
- Detect consecutive, non-newline-delimited NumPy sections by @charliermarsh in #5467
- Fix
unnecessary-encode-utf8
to fixencode
on parenthesized strings correctly by @harupy in #5478 - Allow
Final
assignments in stubs by @charliermarsh in #5490 - Respect
abc
decorators when classifying function types by @charliermarsh in #5315 - Allow
@Author
format for "Missing Author" rule inflake8-todos
by @mayrholu in #4903 - Ignore type aliases for RUF013 by @dhruvmanila in #5344
- Change W605 autofix to use raw strings if possible by @hauntsaninja in #5352
- Add space when migrating to raw string by @charliermarsh in #5358
- Update the
invalid-escape-sequence
rule by @charliermarsh in #5359 - Include BaseException in B017 rule by @charliermarsh in #5466
- [
flake8-django
] Skip duplicate violations inDJ012
by @charliermarsh in #5469