Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AI時代に学ぶ 好きなルール 嫌いなルール Linter編

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

AI時代に学ぶ 好きなルール 嫌いなルール Linter編

Avatar for Shota Nemoto

Shota Nemoto

August 22, 2026

Other Decks in Programming

Transcript

  1. 自己紹介 根本 翔多 Shota Nemoto Recustomer, Inc. / バックエンドエンジニア SIer

    → Web 系 開発では Python を使用し、Linter も使っている 2
  2. Python のチェックには、2つの流派がある ruff mypy 型がなくてもチェックできる Linter いま急速に流行っている 型注釈と実装を照合する型チェッカ 利用者が多い定番 型注釈

    不要 本発表では v0.14.14 型注釈 必要 本発表では v2.1.0 今日はこの2つを、どこまで強くできるかという目線で見ていく 8
  3. ruff を作っている会社は、かの uv を作っている会社 Astral Rust 製の Python ツールを作っている会社 uv

    = Python 本体・パッケージ・仮想環境をまとめて管理するツール(pip・venv・pyenv の置 き換え) uv format を実行すると、uv が ruff を取ってきて動かす 2つは別のツールではなく、1つのツールチェーンになりつつある 12
  4. 歴代 Linter のルールを、1つに内蔵している flake8 UP 定番の検査ルール群 isort import の整列 pyupgrade

    bandit Pylint pycodestyle セキュリティの検査 総合的な検査 新記法への書き換え PEP 8の体裁検査 この6個を含めて、59個の Linter がまるごと入っている 19
  5. デフォルトで有効なのは、この4グループだけ [tool.ruff.lint] select = ["E4", "E7", "E9", "F"] # ←

    何も書かないと、この 4 つ相当 import の書き方 / E7 文の書き方 / E9 構文エラー F Pyflakes — 未定義の名前・未使用の import など ※ 弊社で使っている ruff 0.14時点のデフォルト(最新版の話は後半で) E4 941個のうち、最初から効いているのはごく一部 21
  6. E402 import はファイルの先頭に E4 系 ─ pycodestyle 由来 print("バッチ処理を開始") import

    os # ← 先に処理を書いてしまった # ← import が先頭にない E402 Module level import not at top of file import が散らばると、ファイルの依存関係が一目で追えなくなる 23
  7. E9 壊れた Python の検出 E9 系 ─ pycodestyle 由来 def

    f(: # ← 構文エラー pass invalid-syntax: Expected a parameter or the end of the parameter list いまの ruff は構文エラーを select の設定に関係なく常時チェックしてくれる そもそも実行できないファイルを、実行する前に見つける 24
  8. F401 未使用 import の削除 F 系 ─ Pyflakes 由来 import

    os import json # ← どこでも使っていない from typing import Any # ← これも $ ruff check --fix → Fixed 2 errors import os --fix を付けるだけで、勝手に直る。よくないですか? 25
  9. F821 typo のチェック F 系 ─ Pyflakes 由来 response =

    fetch_order(order_id) if respone.status == "paid": # ← respone(タイポ) ship(order_id) F821 Undefined name `respone` 存在しない名前は書いた瞬間に見つかる 26
  10. F841 未使用変数のチェック F 系 ─ Pyflakes 由来 def apply_discount(order): discounted

    = order.total * 0.9 # ← 計算したのに return order.total # ← 使っていない F841 Local variable `discounted` is assigned to but never used 消し忘れ? それとも返し忘れのバグ? — 必ず立ち止まれる 27
  11. デフォルトだけでも、これだけ守ってくれる E402 E9 F401 F821 F841 import をファイルの先頭に集める 構文エラーの検出 —

    壊れたファイルを実行前に見つける 未使用 import の削除(自動修正つき) typo・存在しない名前のチェック 未使用変数のチェック — バグの匂いに気づける 28
  12. 調べた18リポジトリ GitHub スター上位の著名 Python プロジェクトから、分野が偏らないように選定 fastapi pandas django flask requests

    pydantic httpx airflow home-assistant transformers scikit-learn polars celery sentry mlflow rich openai SDK anthropic SDK 15 / 18 が ruff を採用していた(2026-08時点の設定ファイルを集計) 34
  13. 足されているルールには、人気の定番がある import の整列 B バグの温床を弾く UP 新記法へ書き換え C4 内包表記化 T20

    print の検出 I ただし、多くは 5〜10グループの控えめ運用。ALL は0リポジトリ 14 / 15 10 / 15 10 / 15 6 / 15 4 / 15 35
  14. I001 import の並び、レビューで指摘していませんか 14/15が採用 I 系 ─ isort 由来 import

    requests import os from myapp import models import json Fixed 1 error — 標準ライブラリ → 外部 → 自作の順に自動整列 isort 相当が ruff に内蔵されている。調査では人気1位 38
  15. UP006 / UP007 古い型の書き方、残っていませんか UP は10/15 UP 系 ─ pyupgrade

    由来 from typing import List, Optional def find(ids: List[int]) -> Optional[str]: # ← 3.8 時代の書き方 Fixed → def find(ids: list[int]) -> str | None: Python を上げたら、書き方も自動で追従してくれる 39
  16. UP032 .format() も、勝手に f-string になる UP 系 ─ pyupgrade 由来

    msg = "注文 {} を {} に発送".format(order_id, address) Fixed → msg = f"注文 {order_id} を {address} に発送" UP 系を入れておくと、コードベース全体が新記法に揃い続ける 40
  17. PERF401 for ループを、リスト内包表記に 似た狙いの C4 系は6/15が採用 PERF 系 ─ Perflint

    由来 doubled = [] for price in prices: doubled.append(price * 2) Fixed → doubled = [price * 2 for price in prices] 41
  18. 内包表記のほうが速い — ruff のドキュメントより "the list comprehension is ~10% faster

    on Python 3.11, and ~25% faster on Python 3.10." docs.astral.sh — manual-list-comprehension (PERF401) より 941個すべてのルールに、「なぜ悪いか」のドキュメントが付いている 43
  19. B006 デフォルト引数の [] 、実は使い回されます B は10/15 B 系 ─ flake8-bugbear

    由来 def add_item(item, items=[]): # ← この [] は 1 回しか作られず、共有される items.append(item) return items add_item("a") # ["a"] add_item("b") # ["a", "b"] ← !? B006 Do not use mutable data structures for argument defaults Python の有名な罠を、書いた瞬間に止めてくれる 45
  20. 引数名が、組み込み関数を潰す A002 A 系 ─ flake8-builtins 由来 def get_orders(list, id):

    # ← list() も id() も使えなくなる ... A002 Function argument `list` is shadowing a Python builtin list id type filter — うっかり潰しがちな名前を守る 46
  21. PLE1205 ログの引数、数が合っていない PL 系 ─ Pylint 由来 logger.error("注文 %s の決済に失敗",

    order_id, amount) # ← プレースホルダ 1 つに、引数 2 つ PLE1205 Too many arguments for `logging` format string このミス、エラーログを出すときにしか実行されないから気づけない 47
  22. T201 デバッグの print、本番に置き忘れる T20 は4/15 T20 系 ─ flake8-print 由来

    def calc_fee(order): print(order) # ← デバッグ用のつもりだった return order.total * 0.1 T201 `print` found ログに混ざり続ける print を、push 前に必ず捕まえる 49
  23. S105 パスワードの直書き S 系 ─ bandit 由来 DB_PASSWORD = "hunter2"

    # ← リポジトリに残り続ける S105 Possible hardcoded password assigned to: "DB_PASSWORD" git の履歴に残る前に弾く。セキュリティ系(S)は73ルールある 50
  24. S608 文字列で SQL を組み立てる S 系 ─ bandit 由来 name

    = "' OR '1'='1" # ← 検索欄にこう入力されたら… query = f"SELECT * FROM users WHERE name = '{name}'" # → WHERE name = '' OR '1'='1' に展開され、条件が常に真になる # → 1 人を検索したはずが、全ユーザーの個人情報が返ってくる S608 Possible SQL injection vector through string-based query construction 個人情報の流出につながる SQL インジェクションを、書いた瞬間に指摘 51
  25. ARG001 受け取った引数、使い忘れていませんか ARG 系 ─ flake8-unused-arguments 由来 def send_mail(user, subject,

    body): mail.send(user, subject) # ← body を使い忘れ → 本文が空のメールが飛ぶ!? ARG001 Unused function argument: `body` 受け取ったのに使っていない — さっきの F841(未使用変数)の引数版 52
  26. ERA001 コメントアウトしたコード、残していませんか ERA 系 ─ eradicate 由来 def calc_fee(order): #

    fee = order.total * 0.05 # ← 旧ロジック。いつか使うかも… # if fee > 500: fee = 500 return order.total * 0.1 ERA001 Found commented-out code 履歴は git にある — コードには現役の行だけを残す 54
  27. TD002 / TD003 その TODO、誰がいつやるんですか TD 系 ─ flake8-todos 由来

    def sync_orders(): # TODO: リトライ処理を入れる ← 誰が? どのチケットで? ... TD002 Missing author in TODO TD003 Missing issue link for this TODO 書きっぱなしの TODO を許さない — 担当とチケットを書くまで通らない 55
  28. FBT001 この True 、何の True ですか FBT 系 ─ flake8-boolean-trap

    由来 send_mail(user, True, False) # ← 読めない FBT001 Boolean-typed positional argument in function definition send_mail(user, html=True, retry=False) # ← キーワード引数を強制 呼び出しが読める形にしか書けなくなる 56
  29. PLR2004 その数字、何の数字ですか PL 系 ─ Pylint 由来 if order.status ==

    3: # ← 3 って何? ship(order) PLR2004 Magic value used in comparison if order.status == OrderStatus.PAID: # ← 名前を持たせる 「何を表す数か」は人しか知らない — だから書かせる 57
  30. 全部、入れています 人気リポジトリでは 0件だった ALL 運用 どうしても通らないルールだけ、理由をコメントに書いて ignore する — 現在

    52ルール 801ルールのうち、いま749ルールが有効 有効化できるルールは、全部有効化する 60
  31. ただし、抜いているものもある docstring の有無の強制 — つける強制はしていない 長さ・個数の上限 — 本質的な良し悪しではない E501 /

    PLR09xx 曖昧な Unicode 文字 — 日本語の「!」「?」を使いたい RUF001〜003 assert の使用 — 型の絞り込みでも使うため許可 S101 末尾カンマの強制 — formatter と競合する COM812 D100 〜D415 難しすぎる・意味が薄いと判断したものは、理由を書いて除外している 61
  32. 好きなルール① DTZ005 タイムゾーンのない now() を検出 DTZ 系 ─ flake8-datetimez 由来

    created_at = datetime.now() # ← どこの時刻? 本番サーバは UTC、開発端末は JST — 同じコードで結果が変わる 弊社は海外のお客様にも対応しているので、時刻の扱いは重要 DTZ005 `datetime.datetime.now()` called without a `tz` argument 64
  33. 好きなルール② TID251 禁止 API を、理由つきで宣言する TID 系 ─ flake8-tidy-imports 由来

    [tool.ruff.lint.flake8-tidy-imports.banned-api] "unittest.TestCase".msg = "pytestを使用してください" TID251 `unittest.TestCase` is banned: pytestを使用してください レビューで毎回言っていた決めごとが、設定1行でルールになる 66
  34. 嫌いなルール① D102 ほか 全関数に docstring を書け D 系 ─ pydocstyle

    由来 def get_order(order_id: OrderId) -> Order: """注文IDから注文を取得する""" # ← 型を見れば分かる D102 Missing docstring in public method D 系(docstring の有無チェック)全体を弊社で有効化すると、25,000件超 中身は見ずに有無だけを数える。AI に書かせれば埋まるが、それで何を守れる? 68
  35. 嫌いなルール② E501 / PLR0913 長さと個数を数える系 pycodestyle・Pylint 由来 # E501: 1

    行が長すぎる → formatter がいれば実質不要 # PLR0913: 引数が 6 個以上 → 6 個なら悪くて 5 個なら良い? 上限の根拠は「人間が書いて、人間が読む」前提 書き手が AI に変わると、根拠が薄れる 69
  36. mypy のデフォルト設定は、ぜんぶ「許す」側 [tool.mypy] # 何も書かないと、実質こう disallow_untyped_defs = false # 型注釈のない関数を、許す

    check_untyped_defs = false # 注釈のない関数の中身は、見ない warn_return_any = false # Any が返ってきても、黙っている デフォルトは寛容なモード — 型を書いた場所だけチェックする 73
  37. つまり、型が書いていない所は素通りする def calc_fee(order): return order.total * 0.1 # ← 型注釈なし

    # ← この中は ノーチェック で通る 何も設定しないと、型注釈のない関数は中身のチェックがまるごとスキップされる 「入れてるのに守られていない」が起きるいちばんの原因 74
  38. strict = true — 1行で13個のチェックが入る [tool.mypy] strict = true 型注釈のない関数を許さない(

    disallow_untyped_defs ) Any を返す関数に警告( warn_return_any ) 型の違う値同士の == 比較を警告( strict_equality )… など13個 「型を書いていない場所」が存在できなくなる 77
  39. strict だと、さっきの関数はこうなる def calc_fee(order): return order.total * 0.1 error: Function

    is missing a type annotation [no-untyped-def] def calc_fee(order: Order) -> Decimal: # ← 書くしかない 型を書けば、呼び出し側との食い違いも全部チェックされる 78
  40. 弊社はさらに足している [tool.mypy] strict = true warn_incomplete_stub = true warn_unreachable =

    true enable_error_code = [ # 型スタブの型不足を警告 # 到達しないコードを警告 "unused-awaitable", "possibly-undefined", # await のし忘れ # 未代入の可能性がある変数 "redundant-cast", "truthy-bool", # 不要な cast # __bool__ のない型の真偽値判定 "truthy-iterable", "ignore-without-code", # イテラブルをそのまま真偽値判定 # 理由コードなしの type: ignore "mutable-override", "exhaustive-match", # 可変な属性のオーバーライド # match 文の網羅漏れ "redundant-self", # 不要な self 型注釈 ] strict の先にも、まだ足せるチェックがある 79
  41. 好きなルール③ possibly-undefined 未代入かもしれない変数を検出 mypy ─ enable_error_code if status == "paid":

    label = "支払済" elif status == "pending": label = "支払待ち" send(label) # ← どの分岐も通らなかったら? error: Name "label" may be undefined [possibly-undefined] 分岐の考慮漏れを、実行せずに洗い出す 80
  42. 好きなルール④ exhaustive-match 状態追加時の対応漏れを検出 mypy ─ enable_error_code class Status(Enum): PAID =

    auto() SHIPPED = auto() REFUNDED = auto() # ← 新しく追加した match status: case Status.PAID: notify_paid() case Status.SHIPPED: notify_shipped() # ← REFUNDED は黙って素通り error: Match statement has unhandled case for values of type "Literal[Status.REFUNDED]" [exhaustive-match] AI が指摘してくれることもあるが、設定すれば毎回・全件洗い出される 81
  43. 2026年7月 — ruff 0.16.0で、デフォルトが変わった 59 413 これまでのデフォルト 0.16.0からのデフォルト デフォルトで有効なルールが 59個

    → 413個 に一気に拡大 E711 など主観的な18個は外し、実用的なルールを最初から有効に ruff 自身が、「デフォルト強化」に舵を切った 84
  44. 明日からできること 1. 最新の ruff を入れる、もしくは1グループ足してみる — 0.16ならデフォルトで413ルール。据 え置くなら人気1位の I (import

    整列)から 2. レビューで繰り返し言っている決めごとを、 TID251 に書く — 書く先は pyproject.toml の ruff 設定。AI にも届く 3. mypy は1フラグずつ — まずは check_untyped_defs = true から。型注釈のない関数の中身も チェックされるようになる 86