Ruff v.0.0.292 is out now with full support for Python 3.12. 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.

Support for PEP 701

Python 3.12 was released on October 2nd. We've been preparing for this release for quite some time; for example, we previously announced support for PEP 695 which added new syntax for type declarations. In this release, we're excited to announce support for PEP 701 which provides a formalized grammar for f-strings.

The new grammar lifts many of the restrictions previously imposed on f-strings, as showcased in the following examples, all of which were invalid in earlier versions of Python.

Reuse of enclosing quotes:

f"text { data["key"] } text"

Arbitrary nesting of f-strings:

f"{f"{f"{1 + 1}"}"}"

Note: CPython limits the depth of nested f-strings to 150, and the depth of expression nesting in f-string format specifiers to 2.

# This is fine...
f"{x:{1:{1}}}"

# ...but this isn't.
f"{x:{1:{1:{1}}}}"
# SyntaxError: f-string: expressions nested too deeply

None of the above restrictions are enforced by Ruff.

Multi-line expressions and comments:

f"Some random words {", ".join(
    "alpha"  # first word
    "beta"   # second word
)}"

Backslashes and unicode characters:

f"Some random words: {"\\n".join(words)}"
f"Alpha: \\N{GREEK SMALL LETTER ALPHA}"

For more information, refer to PEP 701 and the 3.12 release notes. You can play around with the above examples on the Ruff Playground.

PEP 701 rule changes

Support for PEP 701 required updating some of Ruff's existing lint rules.

The following rules were updated to detect violations within f-string expressions:

For example, given the following snippet:

f"outer f-string contains placeholder -> {f"inner one doesn't"}"

Ruff will now detect an F541 violation (f-string without any placeholders) on the inner f-string:

$ ruff check --select=F541 --show-source example.py
example.py:1:43: F541 [*] f-string without any placeholders
  |
1 | f"outer f-string contains placeholder -> {f"inner one doesn't"}"
  |                                           ^^^^^^^^^^^^^^^^^^^^ F541
  |
  = help: Remove extraneous `f` prefix

Found 1 error.
[*] 1 potentially fixable with the --fix option.

The flake8-quotes rules are unique in the sense that the diagnostics for nested f-strings should only be reported and fixed if the target-version is 3.12 or later. Currently, only the avoidable-escaped-quote (Q003) rule has been updated to support this while bad-quotes-inline-string (Q000) and bad-quotes-multiline-string (Q001) will only report for the outermost quotes. This will be updated in a coming release.

PEP 701 support was contributed by @dhruvmanila.

Rule change: line-too-long (E501)

The line-too-long (E501) rule now ignores trailing pragma comments (like # type: ignore and # noqa) when computing line length. This is similar to flake8-bugbear's methodology for detecting overlong lines, and ensures that adding pragmas like # noqa does not introduce further lint errors.

See #7692 for more details.

New rule: print-empty-string (FURB105)

What does it do?

Checks for print calls with an empty string as the only positional argument.

Why does it matter?

Prefer calling print without any positional arguments, which is equivalent and more concise.

For example, in the following snippet an empty string is passed to print:

print("")

The "" can be omitted with the same effect:

print()

Derived from refurb.

Contributed by @tjkuson.

New rule: implicit-cwd (FURB177)

What does it do?

Checks for current-directory lookups using Path().resolve().

Why does it matter?

When looking up the current directory, prefer Path.cwd() over Path().resolve(), as Path.cwd() is more explicit in its intent.

For example, in the following snippet an empty path is resolved:

cwd = Path().resolve()

Instead, you should use use the explicit working directory method:

cwd = Path.cwd()

Derived from refurb.

Contributed by @danparizher.

New rule: weak-cryptographic-key (S505)

What does it do?

Checks for uses of cryptographic keys with vulnerable key sizes.

Why does it matter?

Small keys are easily breakable. For DSA and RSA, keys should be at least 2048 bits long. For EC, keys should be at least 224 bits long.

For example, in the following snippet the key size is only 512:

from cryptography.hazmat.primitives.asymmetric import dsa, ec

dsa.generate_private_key(key_size=512)
ec.generate_private_key(curve=ec.SECT163K1)

Instead, a larger key size such as 4096 should be used:

from cryptography.hazmat.primitives.asymmetric import dsa, ec

dsa.generate_private_key(key_size=4096)
ec.generate_private_key(curve=ec.SECP384R1)

Derived from flake8-bandit.

Contributed by @mkniewallner.