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