Slide 1

Slide 1 text

AI 時代に学ぶ 好きなルール・嫌いなルール Linter 編 根本 翔多 PyCon JP 2026 / 2026-08-22

Slide 2

Slide 2 text

自己紹介 根本 翔多 Shota Nemoto Recustomer, Inc. / バックエンドエンジニア SIer → Web 系 開発では Python を使用し、Linter も使っている 2

Slide 3

Slide 3 text

この1年で、 時代がガラッと変わりましたね コードの大半を、AI が書くようになった 3

Slide 4

Slide 4 text

AI が書いたコードの品質、 どうやって担保していますか? 4

Slide 5

Slide 5 text

AI のコードを、AI でレビューする? AI レビュー 文脈から意図を推測して指摘する 確率的なチェック 同じコードでも、指摘が毎回変わる Linter 書かれたルールと照合する 決定的なチェック 同じコードなら、必ず同じ結果で弾く 5

Slide 6

Slide 6 text

Linter は、必ず弾ける 6

Slide 7

Slide 7 text

いまは Linter を強化する時代 に 変わってきたと思いませんか? 今日はこの話をしに来ました 7

Slide 8

Slide 8 text

Python のチェックには、2つの流派がある ruff mypy 型がなくてもチェックできる Linter いま急速に流行っている 型注釈と実装を照合する型チェッカ 利用者が多い定番 型注釈 不要 本発表では v0.14.14 型注釈 必要 本発表では v2.1.0 今日はこの2つを、どこまで強くできるかという目線で見ていく 8

Slide 9

Slide 9 text

Chapter 1 ruff — 型なしで、どこまで守れるか 9

Slide 10

Slide 10 text

ちょっと前まで、この組み合わせでしたよね? black コードの整形 flake8 コードの検査 isort import の整列 別々に入れて、別々に設定して、別々に実行 10

Slide 11

Slide 11 text

いまは、ruff 1つ Rust 製で、圧倒的に速い 11

Slide 12

Slide 12 text

ruff を作っている会社は、かの uv を作っている会社 Astral Rust 製の Python ツールを作っている会社 uv = Python 本体・パッケージ・仮想環境をまとめて管理するツール(pip・venv・pyenv の置 き換え) uv format を実行すると、uv が ruff を取ってきて動かす 2つは別のツールではなく、1つのツールチェーンになりつつある 12

Slide 13

Slide 13 text

Python の道具は、いま Rust 製ファミリーに 置き換わりつつある 13

Slide 14

Slide 14 text

0.62 秒 14

Slide 15

Slide 15 text

コード30万行に対する、ruff の実行時間 0.62秒 Recustomer のソースコード約30万行を、全ルール有効化(ALL)で検査した実測値 15

Slide 16

Slide 16 text

1秒を切ると、開発の仕方が変わる 保存するたびに全ファイル検査できる commit フックに入れても、待たされない CI で毎回全件回しても、誰も気にしない 「たまに回す」から、「常に回っている」へ 16

Slide 17

Slide 17 text

941 個 17

Slide 18

Slide 18 text

ruff が持っているルールの数 941個 うち140個は試験段階(ruff 0.14.14時点) 18

Slide 19

Slide 19 text

歴代 Linter のルールを、1つに内蔵している flake8 UP 定番の検査ルール群 isort import の整列 pyupgrade bandit Pylint pycodestyle セキュリティの検査 総合的な検査 新記法への書き換え PEP 8の体裁検査 この6個を含めて、59個の Linter がまるごと入っている 19

Slide 20

Slide 20 text

ruff がデフォルトで どこまでチェックしてくれるか 20

Slide 21

Slide 21 text

デフォルトで有効なのは、この4グループだけ [tool.ruff.lint] select = ["E4", "E7", "E9", "F"] # ← 何も書かないと、この 4 つ相当 import の書き方 / E7 文の書き方 / E9 構文エラー F Pyflakes — 未定義の名前・未使用の import など ※ 弊社で使っている ruff 0.14時点のデフォルト(最新版の話は後半で) E4 941個のうち、最初から効いているのはごく一部 21

Slide 22

Slide 22 text

デフォルトの4グループが 何を守ってくれるか 22

Slide 23

Slide 23 text

E402 import はファイルの先頭に E4 系 ─ pycodestyle 由来 print("バッチ処理を開始") import os # ← 先に処理を書いてしまった # ← import が先頭にない E402 Module level import not at top of file import が散らばると、ファイルの依存関係が一目で追えなくなる 23

Slide 24

Slide 24 text

E9 壊れた Python の検出 E9 系 ─ pycodestyle 由来 def f(: # ← 構文エラー pass invalid-syntax: Expected a parameter or the end of the parameter list いまの ruff は構文エラーを select の設定に関係なく常時チェックしてくれる そもそも実行できないファイルを、実行する前に見つける 24

Slide 25

Slide 25 text

F401 未使用 import の削除 F 系 ─ Pyflakes 由来 import os import json # ← どこでも使っていない from typing import Any # ← これも $ ruff check --fix → Fixed 2 errors import os --fix を付けるだけで、勝手に直る。よくないですか? 25

Slide 26

Slide 26 text

F821 typo のチェック F 系 ─ Pyflakes 由来 response = fetch_order(order_id) if respone.status == "paid": # ← respone(タイポ) ship(order_id) F821 Undefined name `respone` 存在しない名前は書いた瞬間に見つかる 26

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

デフォルトだけでも、これだけ守ってくれる E402 E9 F401 F821 F841 import をファイルの先頭に集める 構文エラーの検出 — 壊れたファイルを実行前に見つける 未使用 import の削除(自動修正つき) typo・存在しない名前のチェック 未使用変数のチェック — バグの匂いに気づける 28

Slide 29

Slide 29 text

AI 時代 コードが爆速で量産されていく中 これだけで安全なコードを 担保できるでしょうか? 29

Slide 30

Slide 30 text

たぶん、無理 30

Slide 31

Slide 31 text

では ここから強化していきましょう 31

Slide 32

Slide 32 text

みんな、ここから どれだけ足しているのか? 32

Slide 33

Slide 33 text

有名な GitHub リポジトリの Linter 設定を見てみた 33

Slide 34

Slide 34 text

調べた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

Slide 35

Slide 35 text

足されているルールには、人気の定番がある import の整列 B バグの温床を弾く UP 新記法へ書き換え C4 内包表記化 T20 print の検出 I ただし、多くは 5〜10グループの控えめ運用。ALL は0リポジトリ 14 / 15 10 / 15 10 / 15 6 / 15 4 / 15 35

Slide 36

Slide 36 text

ここから 足すと嬉しいルールを 1個ずつ見ていきます さっきの人気定番(I・B・UP・C4・T20)も、この中で紹介します 36

Slide 37

Slide 37 text

Rules 1/5 勝手に直してくれる系 37

Slide 38

Slide 38 text

I001 import の並び、レビューで指摘していませんか 14/15が採用 I 系 ─ isort 由来 import requests import os from myapp import models import json Fixed 1 error — 標準ライブラリ → 外部 → 自作の順に自動整列 isort 相当が ruff に内蔵されている。調査では人気1位 38

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

UP032 .format() も、勝手に f-string になる UP 系 ─ pyupgrade 由来 msg = "注文 {} を {} に発送".format(order_id, address) Fixed → msg = f"注文 {order_id} を {address} に発送" UP 系を入れておくと、コードベース全体が新記法に揃い続ける 40

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

なんでわざわざ 内包表記にするかというと 42

Slide 43

Slide 43 text

内包表記のほうが速い — 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

Slide 44

Slide 44 text

Rules 2/5 バグを必ず弾く系 44

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

引数名が、組み込み関数を潰す 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

Slide 47

Slide 47 text

PLE1205 ログの引数、数が合っていない PL 系 ─ Pylint 由来 logger.error("注文 %s の決済に失敗", order_id, amount) # ← プレースホルダ 1 つに、引数 2 つ PLE1205 Too many arguments for `logging` format string このミス、エラーログを出すときにしか実行されないから気づけない 47

Slide 48

Slide 48 text

Rules 3/5 消し忘れ・うっかりを弾く系 48

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

S105 パスワードの直書き S 系 ─ bandit 由来 DB_PASSWORD = "hunter2" # ← リポジトリに残り続ける S105 Possible hardcoded password assigned to: "DB_PASSWORD" git の履歴に残る前に弾く。セキュリティ系(S)は73ルールある 50

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

ARG001 受け取った引数、使い忘れていませんか ARG 系 ─ flake8-unused-arguments 由来 def send_mail(user, subject, body): mail.send(user, subject) # ← body を使い忘れ → 本文が空のメールが飛ぶ!? ARG001 Unused function argument: `body` 受け取ったのに使っていない — さっきの F841(未使用変数)の引数版 52

Slide 53

Slide 53 text

Rules 4/5 チームの決めごとを守らせる系 53

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

TD002 / TD003 その TODO、誰がいつやるんですか TD 系 ─ flake8-todos 由来 def sync_orders(): # TODO: リトライ処理を入れる ← 誰が? どのチケットで? ... TD002 Missing author in TODO TD003 Missing issue link for this TODO 書きっぱなしの TODO を許さない — 担当とチケットを書くまで通らない 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

PLR2004 その数字、何の数字ですか PL 系 ─ Pylint 由来 if order.status == 3: # ← 3 って何? ship(order) PLR2004 Magic value used in comparison if order.status == OrderStatus.PAID: # ← 名前を持たせる 「何を表す数か」は人しか知らない — だから書かせる 57

Slide 58

Slide 58 text

で、Recustomer は どうしているかというと 58

Slide 59

Slide 59 text

select = ["ALL"] 59

Slide 60

Slide 60 text

全部、入れています 人気リポジトリでは 0件だった ALL 運用 どうしても通らないルールだけ、理由をコメントに書いて ignore する — 現在 52ルール 801ルールのうち、いま749ルールが有効 有効化できるルールは、全部有効化する 60

Slide 61

Slide 61 text

ただし、抜いているものもある docstring の有無の強制 — つける強制はしていない 長さ・個数の上限 — 本質的な良し悪しではない E501 / PLR09xx 曖昧な Unicode 文字 — 日本語の「!」「?」を使いたい RUF001〜003 assert の使用 — 型の絞り込みでも使うため許可 S101 末尾カンマの強制 — formatter と競合する COM812 D100 〜D415 難しすぎる・意味が薄いと判断したものは、理由を書いて除外している 61

Slide 62

Slide 62 text

Chapter 2 好きなルール・嫌いなルール 62

Slide 63

Slide 63 text

いろいろなルールから AI 時代の 好きなルールと嫌いなルールが 見えてきました 63

Slide 64

Slide 64 text

好きなルール① DTZ005 タイムゾーンのない now() を検出 DTZ 系 ─ flake8-datetimez 由来 created_at = datetime.now() # ← どこの時刻? 本番サーバは UTC、開発端末は JST — 同じコードで結果が変わる 弊社は海外のお客様にも対応しているので、時刻の扱いは重要 DTZ005 `datetime.datetime.now()` called without a `tz` argument 64

Slide 65

Slide 65 text

DTZ005 は、autofix がない にするか now(ZoneInfo("Asia/Tokyo")) にするかは、業務が決めること ツールは決められないから、人に決めさせるために止める datetime.now(UTC) 「ツールが直せないもの」こそ、AI 時代に人が守るべき決めごと 65

Slide 66

Slide 66 text

好きなルール② 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

Slide 67

Slide 67 text

TID251 の警告文は、AI がそのまま読む AI はチェックを実行して、警告文を読んで、直してくる msg に書いた日本語が、そのまま AI への指示になる openai / anthropic の SDK リポジトリも、このルールを採用している Linter が、人から AI に情報を伝える 67

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

嫌いなルール② E501 / PLR0913 長さと個数を数える系 pycodestyle・Pylint 由来 # E501: 1 行が長すぎる → formatter がいれば実質不要 # PLR0913: 引数が 6 個以上 → 6 個なら悪くて 5 個なら良い? 上限の根拠は「人間が書いて、人間が読む」前提 書き手が AI に変わると、根拠が薄れる 69

Slide 70

Slide 70 text

好き嫌いの分かれ目 好きなルール 人が決めるべきことを指摘する タイムゾーン・禁止 API・命名 AI が増えるほど効く 嫌いなルール 長さ・有無・個数を数えるだけ 人間の作業量を前提にした上限 AI が書く時代に根拠が薄れる 70

Slide 71

Slide 71 text

Chapter 3 mypy — 型で、もっと厳しくする 71

Slide 72

Slide 72 text

型チェック、 どこまで厳しくしていますか? 72

Slide 73

Slide 73 text

mypy のデフォルト設定は、ぜんぶ「許す」側 [tool.mypy] # 何も書かないと、実質こう disallow_untyped_defs = false # 型注釈のない関数を、許す check_untyped_defs = false # 注釈のない関数の中身は、見ない warn_return_any = false # Any が返ってきても、黙っている デフォルトは寛容なモード — 型を書いた場所だけチェックする 73

Slide 74

Slide 74 text

つまり、型が書いていない所は素通りする def calc_fee(order): return order.total * 0.1 # ← 型注釈なし # ← この中は ノーチェック で通る 何も設定しないと、型注釈のない関数は中身のチェックがまるごとスキップされる 「入れてるのに守られていない」が起きるいちばんの原因 74

Slide 75

Slide 75 text

で、Recustomer は どうしているかというと 75

Slide 76

Slide 76 text

strict = true 76

Slide 77

Slide 77 text

strict = true — 1行で13個のチェックが入る [tool.mypy] strict = true 型注釈のない関数を許さない( disallow_untyped_defs ) Any を返す関数に警告( warn_return_any ) 型の違う値同士の == 比較を警告( strict_equality )… など13個 「型を書いていない場所」が存在できなくなる 77

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

弊社はさらに足している [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

Slide 80

Slide 80 text

好きなルール③ 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

Slide 81

Slide 81 text

好きなルール④ 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

Slide 82

Slide 82 text

厳しくすればするほど AI のコードは安全になる 82

Slide 83

Slide 83 text

実は、先月 83

Slide 84

Slide 84 text

2026年7月 — ruff 0.16.0で、デフォルトが変わった 59 413 これまでのデフォルト 0.16.0からのデフォルト デフォルトで有効なルールが 59個 → 413個 に一気に拡大 E711 など主観的な18個は外し、実用的なルールを最初から有効に ruff 自身が、「デフォルト強化」に舵を切った 84

Slide 85

Slide 85 text

AI 時代は Linter を強化する時代 85

Slide 86

Slide 86 text

明日からできること 1. 最新の ruff を入れる、もしくは1グループ足してみる — 0.16ならデフォルトで413ルール。据 え置くなら人気1位の I (import 整列)から 2. レビューで繰り返し言っている決めごとを、 TID251 に書く — 書く先は pyproject.toml の ruff 設定。AI にも届く 3. mypy は1フラグずつ — まずは check_untyped_defs = true から。型注釈のない関数の中身も チェックされるようになる 86

Slide 87

Slide 87 text

みなさんも ruff ALL 設定 mypy strict 設定 進めていきましょう もちろん、必要に応じて 87

Slide 88

Slide 88 text

と、偉そうなことを言いましたが 88

Slide 89

Slide 89 text

弊社でも もともとは ALL / strict 使っていませんでした 89

Slide 90

Slide 90 text

そこから1年で ALL / strict 適用完了 良いコードベースに なんなら一部は、さらなる堅牢さを求めて Rust 化 90

Slide 91

Slide 91 text

1年あれば、いけます 91

Slide 92

Slide 92 text

私も Linter を真面目に見たのは Recustomer に入社してからです まだ入社1年目 みなさんとそれほど変わらないレベルだと思います 92

Slide 93

Slide 93 text

何か1つでも 参考になるものがあったら うれしいです 93

Slide 94

Slide 94 text

AI 時代 一緒に頑張りましょう 94

Slide 95

Slide 95 text

ところで 95

Slide 96

Slide 96 text

今回 Linter 編でしたが 来年は何編になりますかね? 96

Slide 97

Slide 97 text

ということで 97

Slide 98

Slide 98 text

皆さんまた 来年お会いしましょう! 98

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

ありがとうございました AI 時代の Linter の使い方について、ぜひ情報交換させてください 根本 翔多 / PyCon JP 2026