Ruff 0.17.2 adds a preview rule against repeated regex compilation, plus a batch of false-positive fixes

Mostly false positives: starred type variable constraints, backslash-escaped args in pydocstyle, star elements in pylint. It also marks comment-deleting fixes unsafe and lands two performance changes.

Ruff 0.17.2 shipped on October 22. Patch notes for a linter are usually short, but Ruff’s short notes tend to hide real information like “a rule changed”.

One new preview rule

[complexity] Add unnecessary-regular-expression-compile (#25533)

The rule catches this shape:

import re

# recompiles on every call
def is_phone(s: str) -> bool:
    return re.match(r"^\+?[0-9]{6,15}$", s) is not None

# better: compile once, reuse
PHONE = re.compile(r"^\+?[0-9]{6,15}$")
def is_phone_fast(s: str) -> bool:
    return PHONE.match(s) is not None

Python’s re caches compiled patterns internally (512 by default), so in a small script “compile every time” is invisible. But the cache is keyed on the pattern string, and dynamically built patterns (say, containing user input) keep filling it, the hit rate collapses, and performance falls off a cliff.

The rule is in preview, so it needs an explicit opt-in:

# pyproject.toml
[tool.ruff.lint]
preview = true
select = ["RUF"]   # or add this rule's code specifically

My take: turn preview on, but not everything in it. Some preview rules are still shifting semantics, and enabling all of them means a pile of “this rule behaves differently now” noise at every upgrade.

The Ruff 0.17.2 release page

A batch of false positives

The fixes share a trait: they all live where syntax looks odd but is legal.

Rule What was fixed
UP040 UP046 UP047 RUF053 type variables with starred constraints produced syntax errors
D417 backslash-escaped *args / **kwargs treated as undocumented
PLE0643 false positive on starred elements
PLR5501 PLW0120 UP035 fixes that delete comments are now marked unsafe

The first three are the same story: Python 3.12+ generic syntax (def f[*Ts](...)) broke pattern matching in many older rules. The rules work on the syntax tree, but the helper that asks “is this node shaped like X” is often written loosely, and new syntax trips it.

The fourth matters more: fix deletes comments, and comments are not part of the syntax tree. So an automatic fix can silently remove an explanation you wrote. Those fixes are now marked unsafe, meaning they are not applied by default:

# apply only safe fixes (default)
ruff check --fix .

# allow fixes that change semantics or delete comments
ruff check --fix --unsafe-fixes .

This is the general principle for formatters and linters: auto-fix only what is guaranteed to mean the same thing afterwards.

Two performance changes

  • No intermediate names in lint name resolution (#29506): rule names were re-resolved during matching; dropping the intermediate allocations saves real memory and time.
  • Prefilter bidirectional Unicode checks by UTF-8 leading byte (#29561): the check hunts for bidi control characters in source (the Trojan Source class of problem), and per-character analysis is expensive; checking the leading byte first rules out nearly all ASCII.

The second is the classic “do the cheap test first”: 99% of characters are ASCII, exclude them, then run the expensive check on the rest.

Ruff’s rule index

Why I still use Ruff

I do not write much Python, but every time I do it pays off: one tool replacing flake8, isort, pyupgrade and part of pylint, with one config and a different order of magnitude in speed.

Releases like 0.17.2 need no special attention, but one piece of advice stands: pin the ruff check version. Its rule set moves fast, and a floating version in CI will one day produce dozens of new errors while you are busy with something else.

The PR implementing the new rule

← Back to all posts

Comments

…