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

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

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