Ruff v0.0.285 is out now. Install it from PyPI, or your package manager of choice:
pip install --upgrade ruffAs 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.
Stabilized support for Jupyter Notebooks #
Experimental support for Jupyter Notebooks was added to Ruff in
v0.0.276. With support for IPython magic commands,
--diff for notebooks, and some changes to rule behavior, Ruff's support for Jupyter Notebooks is
considered stable and ready for production use in v0.0.285.
Note Jupyter Notebooks are still not linted by default and requires opt-in by including them in your target paths:
[tool.ruff]
include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"]See the docs on Jupyter Notebook discovery for details.
Jupyter Notebook support was contributed by @dhruvmanila.
Support for IPython magic commands #
IPython magic commands are a
powerful feature in Jupyter Notebooks, allowing you to update IPython settings, run system commands,
access Python help menus, and much more. Initially, Ruff did not support parsing code including
magic commands typically indicated by a leading %, %%, ! since they are not valid Python.
Consequently, cells containing magic commands would be skipped during analysis which could result in
invalid diagnoses.
For example, given the following cells in a notebook:
Cell 1
import pandas as pd
%matplotlib inlineCell 2
df = pd.read_csv("foo.csv")In previous versions of Ruff, an undefined name (F821) violation would be raised in the second
cell since the import is defined in a skipped cell. Now, Ruff supports parsing magic commands in
Jupyter Notebooks and cells including magic commands will be included in the analysis eliminating a
common source of false positives.
Diff support for Jupyter Notebooks #
When using Ruff to apply fixes to violations, it's often useful to preview the changes with the
--diff flag. However, displaying changes to Jupyter Notebooks is challenging due to differences
between the contents of a raw and rendered notebook file. Now, Ruff displays diffs of rendered cells
allowing you to easily validate changes to your notebooks.
For example, here's a diff applying fixes for I001, F841 and EM101 to a notebook:
--- /Users/ruff/notebooks/test.ipynb:cell 1
+++ /Users/ruff/notebooks/test.ipynb:cell 1
@@ -1,2 +1,2 @@
-from pathlib import Path
-import sys
+import sys
+from pathlib import Path
--- /Users/ruff/notebooks/test.ipynb:cell 4
+++ /Users/ruff/notebooks/test.ipynb:cell 4
@@ -1,2 +1,2 @@
def foo():
- unused_variable = "hello world"
+ pass
--- /Users/ruff/notebooks/test.ipynb:cell 6
+++ /Users/ruff/notebooks/test.ipynb:cell 6
@@ -1,2 +1,3 @@
if foo is None:
- raise ValueError("foo cannot be `None`")
+ msg = "foo cannot be `None`"
+ raise ValueError(msg)
Would fix 3 errors.Rule changes for Jupyter Notebooks #
Jupyter Notebooks have slightly different semantics than regular Python. During the experimental period, we discovered a couple rules that needed to be updated to behave differently in notebooks.
The unused-variable (F841) fix will no longer remove the entire statment if the variable is
assigned to a Jupyter magic command. For example, if you have an unused variable assigned to a shell
command as follows:
an_unused_variable = !ping google.comWe would previously suggest deleting the entire line but will now suggest just removing the variable to preserve side-effects:
- an_unused_variable = !ping google.com
+ !ping google.comJupyter Notebooks
allow you to await asynchronous functions
outside of asynchronous contexts. Consequently, yield-outside-function (F704) has been updated
to allow top-level usage of await in Jupyter Notebooks instead of raising a violation.
Rule changes contributed by @dhruvmanila in #6141 and @charliermarsh in #6607
New rule: pytest-unittest-raises-assertion (PT027) #
What does it do? #
Checks for uses of exception-related assertion methods from the unittest module.
Why does it matter? #
To enforce the assertion style recommended by pytest, pytest.raises is preferred over the
exception-related assertion methods in unittest, like assertRaises.
For example, given the following snippet:
import unittest
class TestFoo(unittest.TestCase):
def test_foo(self):
with self.assertRaises(ValueError):
raise ValueError("foo")The unittest style context manager is used but the pytest style context should be used:
import unittest
import pytest
class TestFoo(unittest.TestCase):
def test_foo(self):
with pytest.raises(ValueError):
raise ValueError("foo")See
pytest documentation: Assertions about expected exceptions
for background.
This rule is derived from flake8-pytest-style.
Contributed by @harupy.
New rule: pytest-duplicate-parametrize-test-cases (PT014) #
What does it do? #
Checks for duplicate test cases in pytest.mark.parametrize.
Why does it matter? #
Duplicate test cases are redundant and should be removed.
For example, given the following snippet:
import pytest
@pytest.mark.parametrize(
("param1", "param2"),
[
(1, 2),
(1, 2),
],
)
def test_foo(param1, param2):
...The test case (1, 2) is duplicated and increase test times without increasing coverage. Instead,
omit the redundant case:
import pytest
@pytest.mark.parametrize(
("param1", "param2"),
[
(1, 2),
],
)
def test_foo(param1, param2):
...See
pytest documentation: How to parametrize fixtures and test functions
for background.
This rule is derived from flake8-pytest-style.
Contributed by @harupy.
New rule: banned-module-level-imports (TID253) #
What does it do? #
Checks for module-level imports that should instead be imported lazily (e.g., within a function
definition, or an if TYPE_CHECKING: block, or some other nested context).
Why does it matter? #
Some modules are expensive to import. For example, importing torch or tensorflow can introduce a
noticeable delay in the startup time of a Python program.
In such cases, you may want to enforce that the module is imported lazily as needed, rather than at the top of the file. This could involve inlining the import into the function that uses it, rather than importing it unconditionally, to ensure that the module is only imported when necessary.
For example, given the following snippet:
import tensorflow as tf
def show_version():
print(tf.__version__)tensorflow will be imported when your module is imported but is only needed when show_version is
called. Instead, delay the import:
def show_version():
import tensorflow as tf
print(tf.__version__)This rule requires configuring module names in the flake8-tidy-imports.banned-module-level-imports
setting.
This rule is derived from flake8-tidy-imports.
Contributed by @durumu.
New rule: bad-dunder-name (W3201) #
This rule is available in the Ruff nursery
What does it do? #
Checks for any misspelled dunder name method and for any method defined with __...__ that's not
one of the pre-defined methods.
The pre-defined methods encompass all of Python's standard dunder methods.
Why does it matter? #
Misspelled dunder name methods may cause your code to not function as expected.
Since dunder methods are associated with customizing the behavior of a class in Python, introducing
a dunder method such as __foo__ that diverges from standard Python dunder methods could
potentially confuse someone reading the code.
For example, given the following snippet:
class Foo:
def __init_(self):
...There is a likely typo in the __init__ method definition and it should be written as:
class Foo:
def __init__(self):
...Or given the following snippet:
class Foo:
def __bar__(self):
...__bar__ is not a known dunder method and should probably not be used. Instead, define it as a
private method:
class Foo:
def _bar(self):
...This rule is derived from pylint.
Contributed by @LaBatata101.
New rule: subprocess-run-check (W1510) #
What does it do? #
Checks for uses of subprocess.run without an explicit check argument.
Why does it matter? #
By default, subprocess.run does not check the return code of the process it runs. This can lead to
silent failures.
Instead, consider using check=True to raise an exception if the process fails, or set
check=False explicitly to mark the behavior as intentional.
For example, given the following snippet:
import subprocess
subprocess.run(["ls", "nonexistent"]) # No exception raised.If the command fails, no exception will be raised in Python. Set check to ensure a failed command
is detected:
import subprocess
subprocess.run(["ls", "nonexistent"], check=True) # Raises exception.Or indicate that you do not care if it fails by explicitly setting check to false:
import subprocess
subprocess.run(["ls", "nonexistent"], check=False) # Explicitly no check.See
Python documentation: subprocess.run
for background.
This rule is derived from pylint.
Contributed by @tjkuson.
New rule: quadratic-list-summation (RUF017) #
What does it do? #
Checks for the use of sum() to flatten lists of lists, which has quadratic complexity.
Why does it matter? #
The use of sum() to flatten lists of lists is quadratic in the number of lists, as sum() creates
a new list for each element in the summation.
Instead, consider using another method of flattening lists to avoid quadratic complexity. The following methods are all linear in the number of lists:
functools.reduce(operator.iconcat, lists, [])list(itertools.chain.from_iterable(lists)[item for sublist in lists for item in sublist]
For example, given the following snippet:
lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
joined = sum(lists, [])Flattening the lists will not be performant if they are long. Instead, consider using the following performant implementation:
import functools
import operator
lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
functools.reduce(operator.iconcat, lists, [])See How Not to Flatten a List of Lists in Python or How do I make a flat list out of a list of lists? for background.
Contributed by @evanrittenhouse.