# dosisod/refurb: mypy with one version excluded, and a sample output with an unclosed backtick

> Refurb reports Python modernisation findings on top of mypy, which it depends on directly and pins with one version carved out. Its own configuration surface is the interesting part: enable and disable resolve differently on the command line than in pyproject.toml, and the page ends on a link that stops after an opening parenthesis.

**dosisod/refurb** — A tool for refurbishing and modernizing Python codebases

- Repository: https://github.com/dosisod/refurb
- Stars: 2,533 · Forks: 61
- Language: Python
- License: GPL-3.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/dosisod-refurb

## mypy is a runtime dependency with one specific version carved out

The dependency list is three lines long and one of them decides how the tool behaves. Python is pinned at 3.10 or newer, mypy at 1.10.0 or newer with 1.11.0 explicitly excluded, and tomli at version 2.0.1 or newer but only for interpreters below 3.11. Refurb is therefore a front end over mypy rather than a standalone analyser, and the page treats it that way. Arguments after a bare `--` are handed to mypy, so `refurb files -- --show-traceback` reaches the underlying checker, and the same list can be written into configuration as an array on a mypy_args field. The project lints itself with the same tool it ships: the Makefile runs mypy over the refurb package and over the test directory with the data folders excluded. The exclusion of a single mypy release is the kind of detail that belongs in a release note and is not given a reason anywhere on the page.

## The sample output shows one message with an unclosed backtick

The example runs the tool over a small script and prints four findings:

```
$ refurb main.py
main.py:3:17 [FURB109]: Use `in (x, y, z)` instead of `in [x, y, z]`
main.py:4:5 [FURB101]: Use `y = Path(x).read_text()` instead of `with open(x, ...) as f: y = f.read()`
main.py:10:40 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))`
main.py:16:9 [FURB105]: Use `print() instead of `print("")`
```

Three of the four messages are well formed, quoting the old form and the new form in matched backticks. The fourth does not close its own message. On the FURB105 line an opening backtick sits before the call and there is no matching one after it, so the two quoted fragments run into each other. The sample on the front page therefore demonstrates the output format while showing a message string that renders wrong, on the very check that suggests replacing an empty print with a bare one.

## A disabled check is never loaded, an ignored one is loaded and then silenced

That distinction is the whole suppression model, and it is stated plainly. A disabled check will never be loaded at all, while an ignored check is loaded, an error is emitted, and the error is then suppressed. The flags follow from that. --ignore takes a code that can be written as FURB123 or as 123, and the flag repeats. --enable turns on a check that ships disabled, --disable turns one off, --enable-all opts into everything the project and its plugins offer, and --disable-all turns everything off so checks can be added back incrementally. The precedence rules differ by where you write them. On the command line, whichever of --enable and --disable comes last wins. In the configuration file, disable always wins over enable, and the all flags are applied before the individual entries. Setting both disable_all and enable_all is an error, on the command line and in the file alike.

## Categories are selected with a hash, which is why the example quotes it

Checks can be turned off by category rather than by code, using a hash prefixed syntax: `--disable "#readability"` switches off everything in the readability category, and the same works for enable and ignore. Two details matter in practice. The quotes are in the documented example on purpose, because an unquoted hash starts a comment in the shell and the argument after it would be swallowed before the tool ever saw it. And disabling a whole category does not make its members unreachable, since a check inside a disabled category can still be explicitly re-enabled. That combination is the escape hatch for the category mode: turn the noisy group off, then bring back the two or three inside it that your codebase actually wants. The page does not enumerate the categories, so the check list under docs/ is where you find out what a codebase inherits by default.

## Booleans resolve from the command line while lists merge from both places

Settings can live in pyproject.toml under a tool.refurb table, and the mapping is given as an example rather than described. The command line `refurb file.py --ignore 100 --load some_module --quiet` corresponds to a config block setting ignore to a list holding 100, load to a list holding some_module, and quiet to true. What happens when both places set the same thing is the useful part. For boolean arguments such as --quiet the command line takes precedence, while all other arguments, and ignore and load are named as examples, are combined rather than overridden. So a project level ignore list and an ad hoc one on the command line both apply. A --config-file flag points the tool at a different file for a single run, and that file still has to be in the same TOML form as pyproject.toml. The configuration section closes on the sentence Click [here]( , a link whose target stops after the opening parenthesis.

## 100 percent coverage is the floor, and three modules are carved out of it

The test configuration sets the bar at a number that leaves nothing to negotiate: addopts passes --cov=refurb with an HTML report, a terminal report showing missing lines, and --cov-fail-under=100, so the suite fails on anything less than full coverage of the measured package. Three modules are omitted from the measurement entirely, refurb/__main__.py, refurb/gen.py and refurb/visitor/traverser.py, and the report configuration also skips covered files, skips empty files, and honours three exclusion markers, a pragma comment, an if TYPE_CHECKING block and an assert False. Underneath that sits a strict mypy configuration with allow_redefinition switched back on and the test package granted untyped definitions, plus ruff configured with select set to ALL in preview mode. That last setting has a known conflict written next to it as a TODO about RUF100 not playing well with refurb.

## Expected output files are regenerated by running refurb with --enable-all

The Makefile treats the tool as its own test oracle. A pattern rule turns test/%.py into test/%.txt by running the tool over the source with --enable-all, --quiet and --no-color, redirecting the result into the text file, and appending `|| true` so the rule succeeds even though the run reports findings by design. An update-tests target expands that same pattern across the data directories, which means regenerating the expectations is a matter of running make rather than editing files by hand. There is a separate end to end target that depends on install and points the tool at a dummy file, and the default target chains ruff, mypy, black, isort, typos, pytest, a self check that runs `refurb refurb test/*.py`, and the documentation generator. So the project is refactored by its own output: the same binary a user installs is the binary that decides whether the maintainers own code passes.

## Two output formats, two sort modes, and two separate Python versions

The reporting surface is small on purpose. The default format is text, printed as filename, line, column, bracketed code and message, and the only other format listed is github, for GitHub Annotations, after which the list says more to come. Sorting has two modes, filename as the default and error to sort by code first, settable with --sort or the sort_by field. Two Python versions matter at once: the interpreter running the tool has to be 3.10 or newer, while the --python-version flag tells it which version the code under inspection targets so language feature detection and error messages improve. That argument must be written as x.y. Left unspecified, the tool falls back to the local interpreter and drops the patch level, so a 3.11.5 install is treated as 3.11. The config spelling is python_version = "3.10".

## Conclusion

Refurb earns a place in a repository that already runs mypy in CI, because it reads the same analysis rather than adding a second type checker, and because every finding can be silenced three ways without touching the source. Two things to weigh first. The tool is a reporter, not a fixer: nothing in its documented flags rewrites files, so adopting it means reading findings, not applying patches. And its resolution rules differ between the command line and pyproject.toml, where enable and disable disagree about which one wins, so a policy written in config and enforced in a Makefile is not the same policy. Pin the mypy version you already use, keep the exclusion in mind if that is 1.11.0, and read the check list under docs/ before you decide how much of it to enable.

## FAQ

### What is dosisod/refurb?

It is a tool for refurbishing and modernizing Python codebases, distributed as a console script named refurb from a package of the same name, licensed GPL-3.0-only and requiring Python 3.10 or newer to run.

### How do you make refurb ignore a check?

Three ways: the --ignore flag, whose code can be written FURB123 or 123 and which repeats; an inline comment, either noqa: FURB123 for one check or plain noqa for the whole line; or a category such as --ignore "#readability". The FURB prefix is optional for built in checks and required for plugin checks.

### Can refurb check Python 3.7 code while running on 3.10?

Yes. Refurb itself must run on Python 3.10 or newer, while the --python-version flag, written in x.y form, tells it which version your codebase uses so language features are detected better. Left unset it uses the local interpreter with the patch level dropped.

### Does refurb rewrite your code automatically?

Nothing in the documented flags rewrites files. The example run prints findings in filename, line and column form, and the documented surface covers explaining a check with --explain, ignoring, enabling, disabling, output format and sort order. Arguments after -- are passed through to mypy instead.

### How current is dosisod/refurb?

The default branch master last received a push on 2026-04-03, and that is the same moment the newest tag v2.3.1 was published. Before that, v2.3.0 came on 2026-02-21 and v2.2.0 on 2025-09-17, while the package metadata classifies the project as Production/Stable.

## Sources

- [dosisod/refurb on GitHub](https://github.com/dosisod/refurb)
- [Issues](https://github.com/dosisod/refurb/issues)
- [License: GPL-3.0](https://github.com/dosisod/refurb/blob/master/LICENSE)
- [README](https://github.com/dosisod/refurb/blob/master/README.md)
- [Releases](https://github.com/dosisod/refurb/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dosisod-refurb
