Blog
Python23 min czytania

Ruff — linter i formatter Pythona, który zastępuje Flake8, Blacka i isort

Ruff to napisany w Rust linter i formatter Pythona, który w jednym pliku wykonywalnym mieści ponad 900 reguł, zgodność z Blackiem na poziomie 99,9% linii i sortowanie importów. Pokazujemy kompletną konfigurację w pyproject.toml, różnicę między fixem bezpiecznym a niebezpiecznym, plan migracji z Flake8 i Blacka w jednym commicie oraz dwie pułapki: wersjonowanie 0.x, w którym minor oznacza zmianę łamiącą, i regułę select = ["ALL"].

Ruff to linter i formatter Pythona napisany w Rust, który w jednym pliku wykonywalnym zastępuje Flake8 wraz z kilkudziesięcioma wtyczkami, isort, Blacka, pyupgrade i kilka mniejszych narzędzi. Repozytorium astral-sh/ruff zebrało ponad 49 tysięcy gwiazdek i 2,3 tysiąca forków, wydanie 0.16.3 wyszło 13 sierpnia 2026, a ostatni commit pochodzi z dnia publikacji tego wpisu. Licencja MIT. Pierwszy commit padł w sierpniu 2022, więc projekt ma dokładnie cztery lata.

Listę projektów, które go używają, warto przeczytać w całości, bo mówi więcej niż liczba gwiazdek: Pandas, SciPy, NumPy-owy JAX, PyTorch, FastAPI, Pydantic, Apache Airflow, Apache Superset, Home Assistant, Mozilla Firefox, Jupyter, pytest, Poetry, Polars, Sphinx, Streamlit, LangChain, Hugging Face, Netflix, Databricks MLflow, CERN, IBM Qiskit — i Pylint.

Ostatnia pozycja nie jest pomyłką redakcyjną. Pylint lintuje własny kod Ruffem.

Piszemy o tym z perspektywy zespołu, w którym Python nie jest głównym językiem. W agencji pracującej na Laravelu Python i tak siedzi w kilku miejscach: skrypty deploymentowe i migracyjne, integracje z modelami językowymi, przetwarzanie danych dla klienta, DAG-i w Airflow, generatory dokumentacji. To zwykle kod, którego nikt nie objął takim rygorem jak aplikacji PHP-owej — bo postawienie osobnego pipeline'u z Flake8, isort i Blackiem dla trzech katalogów wygląda na więcej roboty niż pożytku. Z Ruffem ten rachunek się zmienia i to jest właściwy powód, żeby się nim zainteresować.

Pięć narzędzi w jednym pliku wykonywalnym

Typowy zestaw do kontroli jakości kodu w Pythonie wygląda tak: Flake8 z kilkoma wtyczkami, isort do importów, Black do formatowania, pyupgrade do modernizacji składni, do tego czasem eradicate i yesqa. Sześć pakietów, sześć wersji do pilnowania, trzy albo cztery sekcje konfiguracji w różnych plikach (setup.cfg, .flake8, .isort.cfg, pyproject.toml) i tyle samo kroków w CI.

Ruff realizuje to samo jednym plikiem wykonywalnym bez zależności, jedną sekcją konfiguracji i dwoma poleceniami. Ma ponad 900 reguł i jest reimplementacją ponad czterdziestu wtyczek Flake8 — reimplementacją, nie nakładką: każda reguła została napisana od nowa w Rust jako część Ruffa, więc oryginalne wtyczki nie są nigdzie instalowane.

Kody reguł zachowują konwencję Flake8: prefiks literowy plus trzy cyfry. Prefiks mówi, skąd reguła pochodzi:

  • F — Pyflakes: nieużywane importy i zmienne, niezdefiniowane nazwy, błędy f-stringów,
  • E, W — pycodestyle: styl zgodny z PEP 8,
  • I — isort: kolejność i grupowanie importów,
  • N — pep8-naming: konwencje nazewnicze,
  • UP — pyupgrade: składnia przestarzała względem docelowej wersji Pythona,
  • B — flake8-bugbear: konstrukcje, które prawie zawsze są błędem (mutowalne wartości domyślne argumentów, pętle łapiące zmienną przez referencję),
  • S — flake8-bandit: reguły bezpieczeństwa, między innymi subprocess z shell=True, słabe funkcje skrótu, hasła w kodzie,
  • C4 — flake8-comprehensions, SIM — flake8-simplify, RET — flake8-return: uproszczenia kodu,
  • PT — flake8-pytest-style, DJ — flake8-django, FAST — reguły FastAPI, PD — pandas-vet, NPY — NumPy, AIR — Airflow,
  • ANN — flake8-annotations, D — pydocstyle, DOC — pydoclint: adnotacje typów i docstringi,
  • PTH — flake8-use-pathlib, DTZ — flake8-datetimez, LOG i G — logowanie,
  • PL — Pylint, C90 — mccabe (złożoność cyklomatyczna), TRY — tryceratops (obsługa wyjątków), PERF — Perflint, FURB — refurb,
  • T20 — flake8-print (zapomniane print), ERA — eradicate (zakomentowany kod), TD i FIX — TODO i FIXME,
  • RUF — reguły własne Ruffa, bez odpowiednika w żadnej wtyczce.

Pełna lista prefiksów jest dłuższa i obejmuje ponad pięćdziesiąt kategorii. Powyższe wystarczy do zbudowania sensownej konfiguracji; reszta przydaje się, gdy szuka się konkretnej reguły po kodzie w komunikacie błędu.

Prędkość i co z niej realnie wynika

Deklaracja z README brzmi: 10–100 razy szybciej od istniejących linterów i formatterów. Wypowiedzi użytkowników są konkretniejsze. Bryan Van de Ven, współautor Bokeh, podaje 150–200-krotne przyspieszenie względem Flake8: skan całego repozytorium zajmuje 0,2 sekundy zamiast 20. Nick Schrock, współautor GraphQL-a, opisuje przypadek największego modułu w projekcie, który Pylint analizował 2,5 minuty, a Ruff robi to w 0,4 sekundy. Sebastián Ramírez, autor FastAPI, napisał, że czasem wprowadza do kodu celowy błąd, żeby się upewnić, że narzędzie w ogóle działa.

Sama liczba nie jest jeszcze argumentem. Argumentem jest to, co zmienia się w procesie, gdy analiza całego repozytorium schodzi poniżej sekundy:

  • Hook pre-commit przestaje przeszkadzać. Przy dwudziestu sekundach ludzie zaczynają używać --no-verify. Przy 0,2 sekundy nie ma po co.
  • Lintujesz całe repozytorium, nie zmienione pliki. Ograniczanie analizy do diffa jest obejściem powolnego narzędzia i przepuszcza błędy, które ujawniają się dopiero w połączeniu ze zmianą gdzie indziej.
  • Poprawki uruchamiają się przy zapisie pliku. Sortowanie importów i usuwanie nieużywanych nazw dzieje się w edytorze, zanim kod trafi do commita.
  • Krok w CI kosztuje sekundy. To pozwala trzymać lintowanie w osobnym, szybkim jobie, który daje odpowiedź zanim ruszą testy.

Dochodzi jeszcze cache: Ruff zapamiętuje wyniki i nie analizuje ponownie plików, które się nie zmieniły. Przy kolejnym uruchomieniu na niezmienionym drzewie różnica jest jeszcze większa.

Uczciwie: przy projekcie z pięcioma plikami żadna z tych liczb nie ma znaczenia. Ruff ma tam sens z zupełnie innego powodu — bo jest jedną zależnością zamiast sześciu.

Instalacja

Ruff nie ma zależności i nie wymaga Pythona do działania, choć najczęściej instaluje się go razem z projektem. Wariantów jest kilka:

# bez instalacji, jednorazowo
uvx ruff check
uvx ruff format

# jako zależność deweloperska projektu
uv add --dev ruff
pip install ruff

# globalnie
uv tool install ruff@latest
pipx install ruff
brew install ruff

Instalator niezależny od menedżera pakietów, z możliwością wskazania konkretnej wersji:

curl -LsSf https://astral.sh/ruff/install.sh | sh
curl -LsSf https://astral.sh/ruff/0.16.3/install.sh | sh

W CI albo na maszynie, na której nie chcesz nic instalować, najwygodniejszy jest obraz Dockera:

docker run -v .:/io --rm ghcr.io/astral-sh/ruff:0.16.3 check

Obraz ma warianty alpine i debian-slim. Przy Podmanie z SELinuksem wolumen trzeba zamontować z flagą :Z. Są też pakiety w conda-forge, pacmanie, apk i zypperze.

Dwa polecenia i nic więcej

Cały interfejs codzienny to ruff check i ruff format:

ruff check .                    # lintowanie
ruff check --fix .              # z automatyczną poprawką
ruff check --watch .            # w pętli, przy każdej zmianie pliku
ruff check --statistics .       # zestawienie: ile naruszeń której reguły
ruff format .                   # formatowanie
ruff format --check .           # weryfikacja bez zapisu, do CI
ruff format --diff .            # pokaż różnicę, której by dokonał

Kody wyjścia są przewidywalne i to ma znaczenie przy pisaniu skryptów: 0 gdy nie ma naruszeń albo wszystkie zostały naprawione, 1 gdy coś zostało, 2 przy błędzie konfiguracji lub błędzie wewnętrznym. Kod 2 warto obsłużyć osobno — literówka w pyproject.toml nie powinna wyglądać w logu CI tak samo jak nieużywany import.

Dwie flagi zmieniają to zachowanie: --exit-zero wymusza 0 mimo naruszeń (przydatne przy stopniowym wdrażaniu), a --exit-non-zero-on-fix zwraca 1 nawet wtedy, gdy wszystko udało się naprawić — czyli „nie przepuszczaj commita tylko dlatego, że dało się to poprawić automatycznie”.

Domyślnie Ruff analizuje pliki *.py, *.pyi, *.ipynb oraz pyproject.toml. Notatniki Jupytera są lintowane i formatowane od wersji 0.6.0 — jeśli w projekcie leżą notatniki eksploracyjne, których nikt nie chce formatować, trzeba je wykluczyć świadomie.

Konfiguracja: jedna sekcja w pyproject.toml

Ruff czyta konfigurację z pyproject.toml (sekcja [tool.ruff]), z ruff.toml albo z .ruff.toml. Przy kilku plikach w jednym katalogu .ruff.toml wygrywa z ruff.toml, a ten z pyproject.toml. Plik pyproject.toml bez sekcji [tool.ruff] jest po prostu pomijany.

Konfiguracja, od której warto zacząć w projekcie usługowym:

[tool.ruff]
line-length = 100
target-version = "py312"
src = ["src", "tests"]
extend-exclude = ["migrations", "vendor", "notebooks"]

[tool.ruff.lint]
select = [
    "E", "W",    # pycodestyle
    "F",         # Pyflakes
    "I",         # isort
    "N",         # pep8-naming
    "UP",        # pyupgrade
    "B",         # flake8-bugbear
    "C4",        # flake8-comprehensions
    "SIM",       # flake8-simplify
    "PTH",       # flake8-use-pathlib
    "DTZ",       # flake8-datetimez
    "S",         # flake8-bandit
    "T20",       # flake8-print
    "RUF",       # reguły własne Ruffa
]
ignore = [
    "E501",      # długość linii pilnuje formatter
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN", "D"]
"scripts/**/*.py" = ["T20"]
"__init__.py" = ["F401"]

[tool.ruff.lint.isort]
known-first-party = ["twoj_pakiet"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

Kilka rzeczy w tym pliku wymaga komentarza.

E501 na liście ignore to nie niedopatrzenie. Długością linii zajmuje się formatter, a linter zgłaszający ją drugi raz produkuje wyłącznie szum przy stringach i URL-ach, których formatter nie potrafi złamać.

per-file-ignores rozwiązuje trzy typowe konflikty naraz: S101 to „nie używaj assert”, co w testach jest bez sensu; T20 to zakaz print, który w skryptach CLI jest przecież interfejsem użytkownika; F401 w __init__.py to nieużywany import, który w tym pliku bywa świadomym reeksportem.

target-version decyduje o tym, jaką składnię Ruff uzna za przestarzałą. Przy py312 reguły UP przepiszą Optional[int] na int | None, a typing.List na list. Jeśli tego pola nie podasz, Ruff spróbuje odczytać requires-python z pyproject.toml. Warto to ustawić jawnie, bo pomyłka w tę stronę produkuje kod niedziałający na produkcyjnym interpreterze.

docstring-code-format = true włącza formatowanie przykładów kodu wewnątrz docstringów. Domyślnie jest wyłączone i to jedna z tych opcji, o których się nie wie, dopóki się ich nie znajdzie.

Monorepo i dziedziczenie

Konfiguracja jest hierarchiczna na wzór ESLinta: dla każdego pliku obowiązuje najbliższy plik konfiguracyjny idąc w górę drzewa katalogów, a ścieżki względne rozwiązują się względem katalogu tego pliku. Ustawienia nie kaskadują — plik podrzędny nie dziedziczy automatycznie z nadrzędnego. Do dziedziczenia służy jawny extend:

[tool.ruff]
extend = "../../pyproject.toml"
line-length = 120

To rozróżnienie jest źródłem realnych pomyłek w monorepo: dołożenie [tool.ruff.lint] w podkatalogu nie rozszerza konfiguracji z korzenia, tylko ją zastępuje. Argumenty z linii poleceń przebijają wszystko.

Wybór reguł i pułapka select = ["ALL"]

Reguły włącza się przez select (lista zamknięta), extend-select (dołożenie do istniejącej listy) i ignore (wyłączenie). Wskazywać można pełnym kodem (F401) albo prefiksem (F, PL, PLR).

Istnieje też specjalna wartość ALL, włączająca komplet ponad 900 reguł. Ruff automatycznie wyłącza przy niej reguły wzajemnie sprzeczne — na przykład D203 i D211, które opisują dwie niezgodne konwencje pustych linii wokół docstringów. Mimo to select = ["ALL"] jest złym punktem startu dla istniejącego kodu i sama dokumentacja tego nie zaleca.

Powód jest praktyczny. W komplecie siedzą rodziny reguł opisujące wzajemnie wykluczające się style i rodziny, które w konkretnym projekcie są po prostu nietrafione: ANN wymaga adnotacji typów na wszystkim łącznie z self, D wymaga docstringów w każdym module, klasie i funkcji, CPY nagłówka z prawami autorskimi w każdym pliku, FBT zakazuje pozycyjnych argumentów logicznych, EM zabrania literałów w raise. Każda z nich ma sens w jakimś projekcie. Wszystkie naraz w projekcie, który już istnieje, dają kilkanaście tysięcy naruszeń, z czego wartościowa jest garść — i praktyczny efekt jest taki, że zespół wyłącza kolejne prefiksy w losowej kolejności, aż liczba spadnie do znośnej.

Zaczynaj od wąskiej listy reguł, które łapią błędy, a nie preferencje: F, E, W, I, UP, B. To zestaw, którego prawie nikt nie kwestionuje na review i który na starym kodzie daje wynik możliwy do posprzątania w jeden wieczór. Kolejne rodziny dokładaj po jednej, każdą osobnym commitem, po sprzątnięciu poprzedniej. Konfiguracja lintera, przy której zespół dyskutuje o stylu zamiast o kodzie, jest gorsza niż jej brak.

Fixy bezpieczne i niebezpieczne

To najlepiej przemyślana część Ruffa i warto ją zrozumieć, zanim ktoś puści --fix na całym repozytorium.

Ruff dzieli automatyczne poprawki na trzy poziomy stosowalności. Display — poprawka jest pokazywana, ale nigdy nie stosowana. Unsafe — wymaga świadomego włączenia. Safe — stosowana przy --fix.

Poprawka bezpieczna zachowuje zachowanie programu w czasie wykonania i usuwa komentarze wyłącznie wtedy, gdy usuwa całą instrukcję. Poprawka niebezpieczna może zmienić zachowanie albo skasować komentarz. Dokumentacja ilustruje to regułą RUF015, dotyczącą pobrania pierwszego elementu:

# przed
list(range(99999999))[0]

# po
next(iter(range(99999999)))

Zmiana jest o rzędy wielkości szybsza, bo nie materializuje całej listy. Zmienia jednak typ wyjątku przy pustej kolekcji z IndexError na StopIteration. Kod, który ten IndexError łapał, po poprawce przestaje działać — i to jest dokładnie ta klasa zmiany, której nie chcesz dostać automatycznie w commicie „lint fixes”.

ruff check --fix .                    # tylko poprawki bezpieczne
ruff check --fix --unsafe-fixes .     # również niebezpieczne
ruff check --fix-only .               # popraw, nie raportuj reszty

Ustawienia lint.extend-safe-fixes i lint.extend-unsafe-fixes pozwalają przesunąć konkretne reguły między poziomami, jeśli w danym projekcie ocena ryzyka wypada inaczej.

Praktyczna zasada: --fix może chodzić przy zapisie pliku i w hooku pre-commit. --unsafe-fixes uruchamiamy ręcznie, na wydzielonej gałęzi, patrząc na diff, i nigdy w CI.

Wyciszanie naruszeń

Poza ignore i per-file-ignores w konfiguracji są komentarze:

  • # noqa: F401 — wycisza konkretną regułę w tej linii,
  • # noqa — wycisza w tej linii wszystko, czego lepiej nie stosować,
  • # ruff: noqa albo # ruff: noqa: F841 na początku pliku — wycisza w całym pliku.

Do wdrożenia nowej reguły na istniejącym kodzie służy osobna ścieżka:

ruff check --select UP035 --add-noqa .

To polecenie dopisuje komentarze noqa we wszystkich miejscach, w których reguła obecnie zgłasza naruszenie. Efekt: regułę można włączyć od razu dla nowego kodu, a stary dług zostaje oznaczony i policzalny — grep po noqa: UP035 daje listę zadań zamiast mglistego „kiedyś posprzątamy”.

Formatter, czyli Black bez Blacka

ruff format celuje w wynik nieodróżnialny od Blacka i osiąga ponad 99,9% linii sformatowanych identycznie na projektach wcześniej formatowanych Blackiem, takich jak Django czy Zulip. Pozostały ułamek promila to świadome odstępstwa, nie błędy. Warto znać kilka z nich, bo to one wyprodukują diff przy przełączeniu:

  • komentarze pragma (# type, # noqa, # pyright, # pylint) nie są wliczane do szerokości linii,
  • szerokość liczona jest w jednostkach Unicode dla wszystkich tokenów, a nie tylko dla stringów jak w Blacku,
  • puste linie na początku bloku są usuwane, podczas gdy Black od wersji 24 je zostawia,
  • wyrażenia wewnątrz f-stringów są formatowane; Black ich nie rusza,
  • instrukcja z komentarzem na końcu linii jest rozwijana zamiast zwijana, żeby komentarz został przy swoim miejscu,
  • sklejone literały tekstowe są łączone, jeśli mieszczą się w jednej linii,
  • w assert łamana jest wiadomość, a nie warunek,
  • wcięcie pojedynczego wielolinijkowego stringa przekazanego do funkcji jest zachowywane.

Opcji jest więcej niż w Blacku: quote-style (pojedyncze albo podwójne cudzysłowy), indent-style (spacje albo tabulatory), line-length, line-ending, skip-magic-trailing-comma i wspomniane docstring-code-format. Wyłączanie formatowania działa tak samo jak w Blacku, przez # fmt: off, # fmt: on i # fmt: skip; obsługiwane są też pragmy YAPF-a.

Reguły sprzeczne z formatterem

Część reguł lintera opisuje dokładnie to, czym zajmuje się formatter, i włączenie obu naraz kończy się szarpaniem kodu w dwie strony. Ruff to wykrywa i ostrzega przy uruchomieniu — dotyczy to między innymi reguł cudzysłowów (Q000Q004), przecinków końcowych (COM812, COM819) i niektórych reguł wcięć. Uruchomienie ruff format bez ostrzeżeń jest prostym testem poprawności konfiguracji i warto go zrobić raz, po ustawieniu select.

Migracja z Flake8, isort i Blacka

Przy istniejącym projekcie kolejność ma znaczenie, bo od niej zależy, czy dostaniesz jeden czytelny diff, czy trzy nałożone na siebie.

  1. Najpierw sam formatter, osobnym commitem. ruff format . na całym repozytorium, bez żadnych zmian lintera. Diff będzie duży, ale mechaniczny. Hash tego commita wpisz do .git-blame-ignore-revs, żeby git blame nie pokazywał go jako autora każdej linii w projekcie.
  2. Potem wąski zestaw reguł. select = ["F", "E", "W", "I", "UP", "B"] i ruff check --fix .. Importy posortują się same (I), nieużywane znikną (F401), przestarzała składnia zaktualizuje się do target-version (UP).
  3. Reszta ręcznie. To, czego --fix nie ruszy, przejrzyj i albo popraw, albo świadomie dopisz do ignore. Jeśli zostało tego zbyt dużo na jedno posiedzenie, użyj --add-noqa i posprzątaj w kolejnych iteracjach.
  4. Usuń stare narzędzia. Wywal Flake8, isort, Blacka, pyupgrade i ich wtyczki z zależności deweloperskich, skasuj .flake8, setup.cfg i .isort.cfg, wyrzuć ich hooki z pre-commit. Zostawienie któregokolwiek z nich obok Ruffa gwarantuje, że pipeline będzie sobie przeczył.
  5. Na końcu dołóż kolejne rodziny reguł, po jednej, każdą osobnym commitem: SIM, C4, PTH, S, RET.

Przy pierwszym kroku warto uprzedzić zespół, żeby zmergował otwarte gałęzie przed formatowaniem albo policzył się z konfliktami. Formatowanie całego repozytorium w piątek po południu, gdy w toku wisi sześć pull requestów, jest decyzją, którą odczuwa się w poniedziałek.

Pre-commit, CI i edytor

Hook pre-commit — z jednym istotnym szczegółem w kolejności:

- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.16.3
  hooks:
    - id: ruff-check
      args: [ --fix ]
    - id: ruff-format

Dokumentacja jest tu jednoznaczna: przy --fix hook lintera musi stać przed hookiem formattera. Odwrotna kolejność daje kod poprawiony po sformatowaniu, czyli niesformatowany — i drugi przebieg pre-commit, który znów coś zmienia.

W GitHub Actions najprościej użyć gotowej akcji:

name: Ruff
on: [ push, pull_request ]
jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          version: 0.16.3
          args: check --output-format=github

Format wyjścia github sprawia, że naruszenia pojawiają się jako adnotacje przy odpowiednich liniach w widoku pull requesta, a nie tylko w logu. W CI używamy ruff check i ruff format --check — nigdy --fix. Pipeline, który sam poprawia kod i commituje wynik, prędzej czy później nadpisze czyjąś pracę.

Po stronie edytora działa ruff server — serwer języka wbudowany w to samo polecenie, następca osobnego ruff-lsp. Stabilny od wersji 0.5.3. Wtyczka do VS Code nazywa się charliermarsh.ruff, PyCharm od wersji 2025.3 ma obsługę wbudowaną (Settings → Python → Tools → Ruff), Zed ma ją domyślnie, a w Neovimie od 0.11 wystarczy vim.lsp.enable('ruff').

Ustawienie formatowania i poprawek przy zapisie w VS Code:

{
  "[python]": {
    "editor.defaultFormatter": "charliermarsh.ruff",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.fixAll.ruff": "explicit",
      "source.organizeImports.ruff": "explicit"
    }
  }
}

Jedno zastrzeżenie do serwera: obsługuje diagnostykę, poprawki i formatowanie, ale nie nawigację po kodzie ani uzupełnianie. Ma działać obok drugiego serwera języka, nie zamiast niego.

Czego Ruff nie robi

Dwa ograniczenia trzeba znać przed podjęciem decyzji.

Ruff nie jest analizatorem typów. Wyłapie nieużywany import i niezdefiniowaną nazwę, ale nie sprawdzi, czy do funkcji przyjmującej int przekazujesz string. To zadanie mypy, Pyrighta albo — od tego samego producenta — ty, napisanego w Rust na infrastrukturze Ruffa i będącego w połowie 2026 w becie. Te narzędzia są uzupełnieniem, nie alternatywą. Dla zespołu PHP-owego to znajoma sytuacja: Pint formatuje, PHPStan analizuje i nikt nie oczekuje, że jedno zastąpi drugie.

Ruff nie obsługuje wtyczek firm trzecich. Nie napiszesz własnej reguły dla wewnętrznej konwencji zespołu. Dokumentacja pisze, że system wtyczek mieści się w planach projektu, ale dziś dostępne są wyłącznie reguły wbudowane. Jeśli w projekcie działa autorska wtyczka Flake8 pilnująca czegoś specyficznego dla domeny, migracja oznacza jej utratę albo utrzymywanie Flake8 obok Ruffa wyłącznie dla niej.

Jest też różnica w pokryciu względem Pylinta. Ruff ma ponad 900 reguł wobec około 409 w Pylincie, ale zbiory nie zawierają się w sobie — część kontroli Pylinta nie ma jeszcze odpowiednika. Reguły z rodziny PL to podzbiór, nie komplet. Analogicznie z flake8-bugbear: reguły rdzenne są zaimplementowane, część tych opiniotwórczych nie.

Wersja 0.x, czyli dlaczego trzeba przypinać

To pułapka, która przy narzędziu chodzącym w CI potrafi zatrzymać pipeline w środę rano bez żadnej zmiany w kodzie.

Ruff jest w wersjach 0.x i nie stosuje wersjonowania semantycznego. Stosuje własny schemat: numer minor oznacza zmiany łamiące, numer patch poprawki błędów. Zmianą łamiącą jest między innymi usunięcie przestarzałej opcji, niezgodna wstecznie zmiana konfiguracji, promocja reguły do stabilnej i zmiana stabilnego stylu formattera. Wydanie 0.17.0 może więc zgłosić w Twoim kodzie naruszenia, których 0.16.3 nie zgłaszał.

Konsekwencja jest prosta: w CI i w pre-commit przypinaj dokładną wersję. W pliku .pre-commit-config.yaml robi to pole rev, w akcji GitHuba parametr version, w zależnościach deweloperskich zapis ruff==0.16.3. Aktualizację wykonuj świadomie, osobnym pull requestem, patrząc na to, co nowego się zgłosiło.

Osobno działa tryb podglądu (preview = true), włączający reguły i zachowania niestabilne. Nowa reguła trafia najpierw tam i musi przeżyć co najmniej jedno wydanie minor, zanim zostanie uznana za stabilną. Producent zastrzega sobie prawo do zmiany albo usunięcia dowolnego zachowania w tym trybie, więc w pipelinie produkcyjnym nie ma on czego szukać. Do zabawy na lokalnej maszynie — jak najbardziej.

Stabilny styl formattera ma mocniejszą gwarancję: zmienia się wyłącznie po to, by zapobiec wygenerowaniu niepoprawnej składni, zmianie semantyki albo skasowaniu komentarza. Wydania patch nie przemeblują kodu bez powodu.

Kto za tym stoi

Ruff powstaje w firmie Astral, tej samej, która zrobiła menedżer pakietów uv i analizator typów ty. W marcu 2026 Astral ogłosiła dołączenie do OpenAI jako część zespołu Codex; komunikat OpenAI mówi o dalszym wspieraniu narzędzi otwartoźródłowych Astral i rozwijaniu ich publicznie.

Przy wyborze narzędzia do stałego użytku taką zmianę właścicielską wypada odnotować, choć ryzyko jest tu ograniczone. Kod jest na licencji MIT, więc nikt nie może go zamknąć wstecz. Repozytorium jest publiczne, historia commitów kompletna, a przy 49 tysiącach gwiazdek i użyciu w Pandas, PyTorchu czy Home Assistant znalazłby się kto forkować, gdyby rozwój przystanął. Warto to jednak wiedzieć wcześniej niż w trakcie rozmowy z klientem o dostawcach w stosie technologicznym.

Kiedy to ma sens u nas

Konkretnie, dla zespołu, który nie pisze na co dzień w Pythonie:

  • Skrypty pomocnicze w repozytorium aplikacji PHP. Dodanie ruff check do istniejącego pipeline'u to jeden krok w CI i sekundy czasu. Stawianie dla nich Flake8 z wtyczkami rzadko kiedy się opłacało — stąd te katalogi zwykle są nietknięte.
  • Usługi w Pythonie obok głównej aplikacji. Integracje z modelami językowymi, przetwarzanie obrazów, zadania w tle. Kod pisany rzadziej to kod, w którym linter łapie więcej, bo mniej rzeczy siedzi komuś w głowie.
  • Projekty przejęte po kimś. ruff check --statistics . na obcym repozytorium daje w kilka sekund zestawienie naruszeń według reguły. To pierwsze przybliżenie stanu kodu, zanim ktokolwiek go przeczyta.
  • Notatniki i kod analityczny. Notatniki Jupytera są obsługiwane natywnie, więc obejmuje je ta sama konfiguracja co resztę projektu.

Słabiej wypada w dwóch sytuacjach. Pierwsza to projekt z autorskimi wtyczkami Flake8 pilnującymi konwencji domenowych — bez systemu wtyczek nie ma ich jak przenieść. Druga to zespół, który zbudował dużą, dopracowaną konfigurację Pylinta i korzysta z kontroli niemających jeszcze odpowiednika w Ruffie. W obu przypadkach rozwiązaniem bywa Ruff jako szybka warstwa pierwsza plus stare narzędzie w wąskim zakresie, zamiast wymiany jeden do jednego.

Podsumowanie

Co daje Ruff:

  • jeden plik wykonywalny bez zależności w miejsce Flake8 z wtyczkami, isort, Blacka, pyupgrade, eradicate i yesqa,
  • ponad 900 reguł z ponad czterdziestu wtyczek Flake8, zaimplementowanych od nowa w Rust, z zachowaniem oryginalnych kodów,
  • formatter zgodny z Blackiem w ponad 99,9% linii, ze świadomymi i udokumentowanymi odstępstwami,
  • prędkość rzędu 150–200 razy większą od Flake8 przy realnych repozytoriach, plus cache — na tyle, żeby lintować całe drzewo w hooku pre-commit zamiast samego diffa,
  • jedną sekcję konfiguracji w pyproject.toml zamiast czterech plików, z hierarchią dla monorepo i jawnym extend,
  • rozdział poprawek na bezpieczne i niebezpieczne, z --unsafe-fixes jako świadomym wyborem,
  • --add-noqa jako ścieżkę wdrożenia nowej reguły na starym kodzie bez blokowania pipeline'u,
  • gotowe integracje: ruff-pre-commit, astral-sh/ruff-action@v3, obraz ghcr.io/astral-sh/ruff, serwer języka wbudowany w binarkę,
  • projekt w bardzo dobrej kondycji: 49 tysięcy gwiazdek, wydanie 0.16.3 z 13 sierpnia 2026, commity codziennie, używany przez Pandas, SciPy, PyTorch, FastAPI, Airflow i Pylinta.

Na co uważać:

  • wersjonowanie 0.x, w którym minor oznacza zmianę łamiącą — bez przypiętej wersji w CI i pre-commit pipeline potrafi się wywrócić bez żadnej zmiany w kodzie,
  • select = ["ALL"] na istniejącym projekcie daje tysiące naruszeń i dyskusję o stylu zamiast o kodzie — lepiej zacząć od F, E, W, I, UP, B,
  • brak wtyczek firm trzecich — własnej reguły nie napiszesz, a autorska wtyczka Flake8 nie ma jak się przenieść,
  • to nie jest analizator typów; mypy, Pyright albo ty nadal są potrzebne,
  • reguły lintera sprzeczne z formatterem (Q000Q004, COM812) trzeba wyłączyć — ostrzeżenie przy ruff format jest testem, czy konfiguracja jest spójna,
  • przy --fix w pre-commit hook lintera musi stać przed formatterem, inaczej kod wychodzi niesformatowany,
  • pierwsze uruchomienie formattera przemebluje całe repozytorium — osobny commit i wpis w .git-blame-ignore-revs, najlepiej po zmergowaniu otwartych gałęzi.

Licencja: MIT, czyli najbardziej liberalny wariant przy komercyjnym użyciu. Wolno używać, modyfikować i rozprowadzać, także w zamkniętym produkcie, przy zachowaniu noty o prawach autorskich. Ruff jest narzędziem deweloperskim, więc nie trafia do artefaktu wysyłanego klientowi, a nawet gdyby trafił, MIT niczego nie wymusza na kodzie, który powstał przy jego użyciu. Żadnych obowiązków publikacyjnych, żadnego licznika miejsc, żadnej wersji płatnej z innym zestawem funkcji. Warta odnotowania jest za to zmiana właścicielska: w marcu 2026 Astral dołączyła do OpenAI, z deklaracją dalszego rozwijania narzędzi otwartoźródłowych. Licencja MIT sprawia, że nawet niekorzystny obrót spraw zostawia społeczności możliwość forka.