# Ruff Already Rewrites Your Python to Be More Idiomatic


Open any Python codebase that's been through a few hands and you'll find the same fossils: a `for` loop adding items to a set one at a time, a conditional slice that could be `removesuffix()`, a two-line `open()`/`read()` that `pathlib` handles in one call. The code works. It passes every linter you've enabled. And a reviewer who knows Python well would rewrite all of it.

[Ruff](https://pydevtools.com/handbook/reference/ruff.md) can already do that rewriting. It ported 36 rules from [refurb](https://github.com/dosisod/refurb) into a category called `FURB`, and every one of them ships with an auto-fix. Most projects haven't turned them on because Ruff's defaults only enable Pyflakes (`F`) and a subset of pycodestyle (`E`). The rules that catch non-idiomatic patterns sit behind an opt-in flag, invisible to the people who'd benefit most.

Here are three patterns you've probably written this month.

## The set-add loop

```python
s = set()
for x in items:
    s.add(x)
```

FURB142 rewrites this to `s.update(items)`. One method call instead of N, and the intent is immediately clear: you're building a set from an iterable, not doing something conditional inside the loop. The same logic applies to consecutive `.append()` calls (FURB113 collapses them into `.extend()`).

This is the kind of pattern that persists because it's never *wrong*. No test catches it. No type checker flags it. It just sits there, making the reader do slightly more work than necessary on every encounter.

## The hand-rolled removesuffix

Python 3.9 added `str.removeprefix()` and `str.removesuffix()`, but codebases that predate 3.9 still carry the old conditional slice:

```python
filename = filename[:-4] if filename.endswith(".txt") else filename

if text.startswith("pre"):
    text = text[3:]
```

FURB188 rewrites both:

```python
filename = filename.removesuffix(".txt")
text = text.removeprefix("pre")
```

The `removesuffix` version is shorter, but the real win is eliminating off-by-one bugs. When someone changes the suffix from `".txt"` to `".json"` and forgets to update the slice from `[:-4]` to `[:-5]`, the old version silently mangles filenames. The new version can't have that bug.

## The open/read two-step

```python
with open("config.json") as f:
    data = f.read()
```

FURB101 collapses this to `Path("config.json").read_text()`. It's the same operation in fewer moving parts. `pathlib` has shipped this method since Python 3.5, but the `open()`/`read()` pattern is so deeply embedded in tutorials and muscle memory that it persists even in codebases that use `Path` everywhere else.

## Turn them on

Add two keys to [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md):

```toml
[tool.ruff]
preview = true

[tool.ruff.lint]
extend-select = ["FURB"]
```

Many FURB rules are still in Ruff's preview tier, so `preview = true` is needed to activate them. Then `ruff check --fix .` applies all safe fixes at once.

FURB covers different ground than the two related categories you may already have enabled. `UP` (pyupgrade) modernizes syntax: f-strings over `%`-formatting, `X | Y` over `Union[X, Y]`. `SIM` (flake8-simplify) simplifies control flow: collapsing nested `if` statements, rewriting `not not x`. `FURB` targets a different layer: hand-rolled logic that the standard library already handles. The three don't overlap and don't conflict. The [Ruff complete guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) covers all of them, and [configuring recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md) includes `FURB` in the curated rule set.

## Where this comes from

The standalone [refurb](https://github.com/dosisod/refurb) tool (2.5k GitHub stars, created by dosisod) originated these rules. It runs on [mypy](https://pydevtools.com/handbook/reference/mypy.md)'s analysis engine and catches some patterns Ruff hasn't ported yet. For most projects, Ruff's `FURB` subset covers the patterns that matter, and you get them without adding another tool to the chain.

The full list of 36 rules is in the [Ruff FURB rules reference](https://docs.astral.sh/ruff/rules/#refurb-furb).
