mypyとpyrightは、どちらもPythonの型ヒントをもとにコードを実行せずに誤りを見つける「静的型チェッカー」で、mypyはPython公式に近い標準的な実装、pyrightはMicrosoft製でVS Code(Pylance)の中身にもなっている実装です。同じコードでも、注釈のない関数を調べるかどうか、分岐後の型をどう推論するかといった考え方が違うため、片方では通るコードがもう片方では指摘されることがあります。

この記事では、mypy 2.3.1とpyright 1.1.414に同じサンプルコードをかけて、出力をそのまま並べて比較します。さらに、コピペで使える pyproject.toml([tool.mypy] と [tool.pyright])、既存コードに段階的に入れる手順、pre-commitとGitHub Actionsの設定、実行時間の実測、VS Codeとの関係、types-* スタブの入れ方、つまずきやすいエラーの直し方までまとめました。型ヒントそのものの書き方は扱わず、「ツールをどう使うか」に絞っています。

mypyとpyrightの比較早見表

観点 mypy pyright
開発元 Python型ヒントの原点となったプロジェクト(Dropbox発、現在はコミュニティ) Microsoft
実装言語 Python(mypycでコンパイル済み配布) TypeScript(Node.js上で動く)
最新版(2026-09-30時点、PyPI/npm) 2.3.1(2.0でPython 3.9対応終了、--num-workers の並列チェックが追加) 1.1.414(週次に近いペースで更新)
インストール pip install mypy pip install pyright(Node.jsを内包/取得するラッパー)または npm i -g pyright
設定ファイル pyproject.toml の [tool.mypy]、mypy.ini、setup.cfg pyproject.toml の [tool.pyright]、pyrightconfig.json(両方あればJSONが優先)
厳しさの既定 緩め(注釈のない関数の中身は調べない) standard(注釈がなくても全コードを調べる)。Pylanceの既定は off
strictにする方法 --strict または strict = true typeCheckingMode = "strict" またはファイル先頭に # pyright: strict
注釈のない関数 既定では本文をチェックしない(--check-untyped-defs で有効化) 常にチェックし、戻り値も呼び出し側の引数から推論する
分岐後の型 join(共通の親型に丸める) union(str | int のまま保つ)
戻り値の推論 注釈がなければ Any return文から推論
到達不能コード --warn-unreachable で報告 エディタ上でグレー表示(CLIでは reportUnnecessaryIsInstance などで間接的に検出)
抑止コメント # type: ignore[error-code] # pyright: ignore[ruleName](# type: ignore も既定で有効)
未使用の抑止コメント warn_unused_ignores(strictに含まれる) reportUnnecessaryTypeIgnoreComment(strictにも含まれないので明示)
エディタ連携 VS Code拡張「Mypy Type Checker」、dmypy(デーモン)で高速化 VS CodeはPylanceの中身。ほかのエディタは pyright-langserver
プラグイン あり(django-stubs など) なし(速度と保守性のため)
並列化 --num-workers N(2.0以降、実験的) --threads
出力形式 テキスト(--pretty でソース行つき)、--output json テキスト、--outputjson
実行時間(この記事の実測、2コア環境) rich 15.0.0(100ファイル・約2.6万行)を 3.2秒、キャッシュありなら 0.14秒 同じコードを 13.9秒(キャッシュなし)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「mypyとpyrightの違いと使い方|設定例・CI導入・出力比較」

どちらを選ぶかの判断表

状況 おすすめ 理由
VS Codeで開発している pyright(Pylance) エディタの赤線とCLIの結果が一致する。CLIもpyrightにすると差異が出ない
Djangoなどプラグイン前提のライブラリを使う mypy django-stubs のようなプラグインはmypy専用
型ヒントがまだ少ないコードベース mypy から 既定が緩く、注釈のある関数から順に広げやすい
注釈なしのコードでも誤りを拾いたい pyright 未注釈の関数も推論して検出する(この記事の実行結果を参照)
CIの時間を短くしたい mypy(キャッシュ活用)または新しいチェッカー .mypy_cache を残せば2回目以降は数百ms。速度重視ならty/pyreflyも候補
両方通したい 両方をCIに 検出範囲が少しずつ違うので、pre-commitに両方入れる構成も現実的(後述)

迷ったら「エディタで使っているものをCIにも使う」が基本です。VS Codeなら pyright、それ以外や既存のmypy資産があるなら mypy を選び、必要になったらもう片方を足すのが手戻りの少ない進め方です。

インストールして最初に実行する

検証環境は Python 3.11.15、mypy 2.3.1(compiled: yes)、pyright 1.1.414 です。仮想環境(.venv)を作ってから入れます。

python3 -m venv .venv
. .venv/bin/activate
pip install mypy==2.3.1 pyright==1.1.414
mypy --version
pyright --version
mypy 2.3.1 (compiled: yes)
pyright 1.1.414

pip版のpyrightはNode.js上で動く本体を呼び出すラッパーです(Node.jsが無い環境では初回に取得します)。npmで入れる場合は npm i -g pyright です。終了コードは、どちらもエラーがあれば1、なければ0なので、そのままCIの合否に使えます。

同じコードを両方にかけて検出の違いを見る

Optional・再代入・ナローイング・注釈なし関数・戻り値推論・Any・TypedDict・Protocol・overload・ジェネリクス・到達不能コードを1ファイルに詰め込んだサンプルです。reveal_type() はチェッカーが「その式をどの型と見ているか」を表示する関数で、実行時には存在しませんが型チェック時だけ使えます。

"""sample.py : mypy と pyright に同じコードをかけて違いを見る"""
from typing import Any, Protocol, TypedDict, TypeVar, overload


# 1. Optional(None を含む戻り値)をそのまま使う
class Item:
    def __init__(self, name: str) -> None:
        self.name = name


def find_item(items: list[Item], name: str) -> Item | None:
    for item in items:
        if item.name == name:
            return item
    return None


def case1_optional(items: list[Item]) -> str:
    found = find_item(items, "pen")
    return found.name  # None の可能性


# 2. 分岐で別の型を代入
def case2_reassign(flag: bool) -> None:
    if flag:
        value = 3
    else:
        value = "three"
    reveal_type(value)


# 3. isinstance で分岐した後の型(union か join か)
def case3_join(val: str | int) -> None:
    if isinstance(val, str):
        result = val
    else:
        result = val
    reveal_type(result)


# 4. 注釈なし関数(本文をチェックするか、呼び出し側で推論するか)
def case4_untyped(a, b):
    return a + b


def case4_body():
    return 1 + "a"  # 注釈なし関数の中の明らかな誤り


def case4_caller() -> None:
    n = case4_untyped(1, 2)
    reveal_type(n)
    n.upper()  # int に upper() はない


# 5. 戻り値型の推論
def case5_make_count():
    return 42


def case5_use() -> None:
    label = case5_make_count() + "件"  # int + str
    reveal_type(label)


# 6. Any
def case6_any(raw: Any) -> None:
    raw.anything()  # Any には何をしても通る
    text: str = raw
    reveal_type(text)


# 7. TypedDict
class Order(TypedDict):
    order_id: int
    note: str


def case7_typeddict() -> None:
    order: Order = {"order_id": 1}  # note が足りない
    print(order["memo"])  # 存在しないキー


# 8. Protocol
class HasArea(Protocol):
    def area(self) -> float: ...


class Square:
    def __init__(self, side: float) -> None:
        self.side = side

    def area(self) -> float:
        return self.side**2


class Label:
    text = "label"


def total_area(shapes: list[HasArea]) -> float:
    return sum(s.area() for s in shapes)


def case8_protocol() -> None:
    total_area([Square(2.0)])
    total_area([Label()])  # area() がない


# 9. overload
@overload
def parse(value: int) -> int: ...
@overload
def parse(value: str) -> str: ...
def parse(value: int | str) -> int | str:
    return value


def case9_overload(raw: Any) -> None:
    parse(1.5)  # float は overload にない
    reveal_type(parse(raw))


# 10. ジェネリクス
T = TypeVar("T")


def first(items: list[T]) -> T:
    return items[0]


def case10_generics() -> None:
    reveal_type(first([1, 2, 3]))
    mixed = [1, 3.4, ""]
    reveal_type(mixed)
    pair = (1, "stop")
    reveal_type(pair[1])


# 11. 到達不能コード
def case11_unreachable(count: int) -> str:
    if isinstance(count, int):
        return "number"
    return "other"  # ここには到達しない

mypy(既定設定)の出力

$ mypy sample.py
sample.py:20: error: Item "None" of "Item | None" has no attribute "name"  [union-attr]
sample.py:28: error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
sample.py:29: note: Revealed type is "int"
sample.py:37: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
sample.py:38: note: Revealed type is "str"
sample.py:52: note: Revealed type is "Any"
sample.py:63: note: Revealed type is "Any"
sample.py:70: note: Revealed type is "str"
sample.py:80: error: Missing key "note" for TypedDict "Order"  [typeddict-item]
sample.py:81: error: TypedDict "Order" has no key "memo"  [typeddict-item]
sample.py:107: error: List item 0 has incompatible type "Label"; expected "HasArea"  [list-item]
sample.py:120: error: No overload variant of "parse" matches argument type "float"  [call-overload]
sample.py:120: note: Possible overload variants:
sample.py:120: note:     def parse(value: int) -> int
sample.py:120: note:     def parse(value: str) -> str
sample.py:121: note: Revealed type is "Any"
sample.py:133: note: Revealed type is "int"
sample.py:135: note: Revealed type is "list[object]"
sample.py:137: note: Revealed type is "str"
Found 7 errors in 1 file (checked 1 source file)

pyright(既定=standard)の出力

$ pyright sample.py
sample.py
  sample.py:20:18 - error: "name" is not a known attribute of "None" (reportOptionalMemberAccess)
  sample.py:29:17 - information: Type of "value" is "Literal[3, 'three']"
  sample.py:38:17 - information: Type of "result" is "str | int"
  sample.py:47:12 - error: Operator "+" not supported for types "Literal[1]" and "Literal['a']" (reportOperatorIssue)
  sample.py:52:17 - information: Type of "n" is "Literal[3]"
  sample.py:53:7 - error: Cannot access attribute "upper" for class "Literal[3]"
    Attribute "upper" is unknown (reportAttributeAccessIssue)
  sample.py:62:13 - error: Operator "+" not supported for types "Literal[42]" and "Literal['件']" (reportOperatorIssue)
  sample.py:63:17 - information: Type of "label" is "Unknown"
  sample.py:70:17 - information: Type of "text" is "str"
  sample.py:80:20 - error: Type "dict[str, int]" is not assignable to declared type "Order"
    "note" is required in "Order" (reportAssignmentType)
  sample.py:81:11 - error: Could not access item in TypedDict
    "memo" is not a defined key in "Order" (reportGeneralTypeIssues)
  sample.py:107:17 - error: Argument of type "list[Label]" cannot be assigned to parameter "shapes" of type "list[HasArea]" in function "total_area"
    "Label" is incompatible with protocol "HasArea"
      "area" is not present (reportArgumentType)
  sample.py:120:5 - error: No overloads for "parse" match the provided arguments (reportCallIssue)
  sample.py:120:11 - error: Argument of type "float" cannot be assigned to parameter "value" of type "str" in function "parse"
    "float" is not assignable to "str" (reportArgumentType)
  sample.py:121:17 - information: Type of "parse(raw)" is "Unknown"
  sample.py:133:17 - information: Type of "first([1, 2, 3])" is "int"
  sample.py:135:17 - information: Type of "mixed" is "list[Unknown]"
  sample.py:137:17 - information: Type of "pair[1]" is "Literal['stop']"
9 errors, 0 warnings, 9 informations

ケース別:どちらが何を検出したか

ケース mypy(既定) pyright(standard) 違いの理由
1. Optionalの属性アクセス error [union-attr] error reportOptionalMemberAccess 両方が検出。最も実務で効く指摘
2. 分岐で int と str を代入 error [assignment](最初の代入で型が決まる) 通る。型は Literal[3, 'three'] mypyは最初の代入を宣言とみなす。pyrightは代入ごとに型を追う
3. isinstance後の再代入 error(str に int を代入) 通る。str | int(union) mypyはjoin、pyrightはunionで型を保つ
4. 注釈なし関数の本文 1 + "a" 通る(本文を調べない) error reportOperatorIssue mypyは --check-untyped-defs で検出できる
4. 注釈なし関数の戻り値 n.upper() 通る(n は Any) error(n は Literal[3]) pyrightは呼び出し側の引数から戻り値を推論する
5. 戻り値推論 42 + "件" 通る(Any) error reportOperatorIssue pyrightはreturn文から戻り値型を推論する
6. Anyへの操作 通る 通る Any は両方で「何でもあり」。pyrightは暗黙のAnyを Unknown と区別する
7. TypedDictのキー不足・未定義キー error [typeddict-item] ×2 error ×2 両方が検出。文言が違うだけ
8. Protocolを満たさないクラス error [list-item] error reportArgumentType(欠けているメソッド名まで表示) 両方が検出。pyrightの方が原因の説明が長い
9. overloadに合わない引数 error [call-overload](候補一覧つき) error ×2 両方が検出
9. overloadに Any を渡した結果 Any Unknown どちらも決められないので Any 扱い
10. [1, 3.4, ""] の型 list[object] list[Unknown](strictでは list[int | float | str]) joinとunionの違いがここにも出る
10. (1, "stop")[1] の型 str Literal['stop'] pyrightはタプルのリテラル型を保つ
11. 到達不能なreturn 通る(--warn-unreachable で検出) 通る(strictで reportUnnecessaryIsInstance) 次の節を参照

既定設定では、pyrightの方が「注釈のないコード」に強く、mypyの方が「型を1つに決める」ことに厳格です。どちらが正しいというより設計思想の違いなので、チームでどちらかに揃えることが大切です。

strictにすると何が増えるか

mypy --strict は複数のフラグをまとめて有効にするスイッチです。mypy --help に一覧が出ます。

--strict   Strict mode; enables the following flags:
           --disallow-any-generics, --disallow-subclassing-any, --disallow-untyped-calls,
           --disallow-untyped-defs, --disallow-incomplete-defs, --check-untyped-defs,
           --disallow-untyped-decorators, --warn-redundant-casts, --warn-unused-ignores,
           --warn-return-any, --no-implicit-reexport, --strict-equality, --extra-checks

--warn-unreachable はstrictに含まれないので、到達不能コードも見たいときは足します。同じサンプルにかけると、既定の7件から14件に増えました(差分だけ抜粋)。

$ mypy --strict --warn-unreachable sample.py
sample.py:42: error: Function is missing a type annotation  [no-untyped-def]
sample.py:46: error: Function is missing a return type annotation  [no-untyped-def]
sample.py:47: error: Unsupported operand types for + ("int" and "str")  [operator]
sample.py:51: error: Call to untyped function "case4_untyped" in typed context  [no-untyped-call]
sample.py:57: error: Function is missing a return type annotation  [no-untyped-def]
sample.py:62: error: Call to untyped function "case5_make_count" in typed context  [no-untyped-call]
sample.py:144: error: Statement is unreachable  [unreachable]
Found 14 errors in 1 file (checked 1 source file)

pyrightのCLIには --strict フラグがなく、設定ファイルの typeCheckingMode かファイル先頭の # pyright: strict で切り替えます。strictでは「型がわからない(Unknown)」ことそのものが指摘対象になり、9件から21件に増えました(差分だけ抜粋)。

$ pyright sample.py   # pyrightconfig.json: {"typeCheckingMode": "strict"}
  sample.py:42:5 - error: Return type is unknown (reportUnknownParameterType)
  sample.py:42:19 - error: Type of parameter "a" is unknown (reportUnknownParameterType)
  sample.py:42:19 - error: Type annotation is missing for parameter "a" (reportMissingParameterType)
  sample.py:43:12 - error: Return type is unknown (reportUnknownVariableType)
  sample.py:53:5 - error: Type of "upper" is unknown (reportUnknownMemberType)
  sample.py:62:5 - error: Type of "label" is unknown (reportUnknownVariableType)
  sample.py:81:11 - error: Argument type is unknown
    Argument corresponds to parameter "values" in function "print" (reportUnknownArgumentType)
  sample.py:135:17 - information: Type of "mixed" is "list[int | float | str]"
  sample.py:142:8 - error: Unnecessary isinstance call; "int" is always an instance of "int" (reportUnnecessaryIsInstance)
21 errors, 0 warnings, 9 informations
strictで増える指摘 mypy pyright
注釈のない関数定義 [no-untyped-def] reportMissingParameterType、reportUnknownParameterType
注釈のない関数の呼び出し [no-untyped-call] (呼び出し自体は可。結果がUnknownなら reportUnknownVariableType)
型が決まらない変数・引数・戻り値 [no-any-return](Anyを返す関数) reportUnknown* 一式(Variable/Member/Argument/Parameter)
不要な cast / isinstance [redundant-cast] reportUnnecessaryCast、reportUnnecessaryIsInstance
未使用の # type: ignore [unused-ignore] 含まれない(reportUnnecessaryTypeIgnoreComment を明示)
list など引数なしのジェネリック [type-arg] reportMissingTypeArgument

コピペ用の設定ファイル(pyproject.toml と pyrightconfig.json)

1つの pyproject.toml に両方の設定を置けます。「本体はstrict、古いモジュールとテストは緩める、スタブのないライブラリは黙らせる」という実務でよく使う形です。

[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.11"

# ---------- mypy ----------
[tool.mypy]
python_version = "3.11"
files = ["src", "tests"]          # 対象。mypy 実行時に引数なしでこれを見る
mypy_path = "src"                  # src レイアウトの解決用
strict = true                      # --strict 相当(個別フラグをまとめて有効化)
warn_unreachable = true            # 到達不能コードを警告(strict には含まれない)
show_error_codes = true            # [error-code] を表示(2.x では既定で on)
pretty = false                     # true にするとソース行つきで表示
exclude = ["build/", "dist/"]

# 未整備の古いモジュールは段階的に:まず本文チェックだけ、注釈は要求しない
[[tool.mypy.overrides]]
module = ["app.legacy", "app.legacy.*"]
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true

# 型スタブのないサードパーティは import エラーを抑止
[[tool.mypy.overrides]]
module = ["somelib", "somelib.*", "yaml"]
ignore_missing_imports = true

# テストは untyped 呼び出しを許可
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_calls = false

# ---------- pyright ----------
[tool.pyright]
include = ["src", "tests"]
exclude = ["**/__pycache__", "build", "dist"]
pythonVersion = "3.11"
typeCheckingMode = "strict"        # off / basic / standard(既定) / strict
strict = ["src/app/core.py"]       # 特定ファイルだけ strict にすることも可
venvPath = "."                     # 仮想環境のある親ディレクトリ
venv = ".venv"                     # 仮想環境の名前(コマンド行 --venvpath でも可)
reportMissingImports = "error"
reportMissingTypeStubs = "none"    # スタブなしのライブラリを許容
reportUnknownMemberType = "warning"
reportUnknownArgumentType = "warning"

# 一部ディレクトリだけ設定を変える
[[tool.pyright.executionEnvironments]]
root = "src/app/legacy"
reportMissingParameterType = "none"
reportUnknownParameterType = "none"
reportUnknownVariableType = "none"

JSONで持ちたい場合の pyrightconfig.json は次のとおりです(pyproject.toml と両方あるとJSONが優先されます)。

{
  "include": ["src", "tests"],
  "exclude": ["**/__pycache__", "build", "dist"],
  "pythonVersion": "3.11",
  "typeCheckingMode": "strict",
  "venvPath": ".",
  "venv": ".venv",
  "reportMissingImports": "error",
  "reportMissingTypeStubs": "none",
  "reportUnknownMemberType": "warning",
  "reportUnknownArgumentType": "warning",
  "executionEnvironments": [
    {
      "root": "src/app/legacy",
      "reportMissingParameterType": "none",
      "reportUnknownParameterType": "none",
      "reportUnknownVariableType": "none"
    }
  ]
}

この設定で src/app/core.py(注釈あり)、src/app/legacy/report.py(注釈なし、import somelib あり)、tests/test_core.py を含むプロジェクトを検証しました。mypyは引数なしで files を読み、pyrightは include を読みます。

$ mypy
Success: no issues found in 6 source files

$ pyright
src/app/legacy/report.py
  src/app/legacy/report.py:1:8 - error: Import "somelib" could not be resolved (reportMissingImports)
  src/app/legacy/report.py:7:9 - warning: Type of "append" is partially unknown
    Type of "append" is "(object: Unknown, /) -> None" (reportUnknownMemberType)
  ...
2 errors, 3 warnings, 0 informations

pyrightには mypy の ignore_missing_imports のような「モジュール名ごとの抑止」がないため、存在しないライブラリのimportは行単位の # pyright: ignore[reportMissingImports] か、executionEnvironments の reportMissingImports = "none" で扱います。行コメントを付けると 0 errors になりました。

設定の対応表 mypy([tool.mypy]) pyright([tool.pyright])
対象ファイル files / exclude include / exclude
Pythonバージョン python_version = "3.11" pythonVersion = "3.11"
厳しさ strict = true typeCheckingMode = "strict"
一部だけ緩める/厳しくする [[tool.mypy.overrides]] + module = [...] [[tool.pyright.executionEnvironments]] + root、または strict = ["path"]
スタブのないライブラリ ignore_missing_imports = true(overridesで) reportMissingTypeStubs = "none"、未解決は reportMissingImports
仮想環境の指定 python_executable = ".venv/bin/python" venvPath + venv、または --pythonpath
到達不能コード warn_unreachable = true (エディタのグレー表示。reportUnnecessaryComparison などで補う)
未使用のignore warn_unused_ignores = true reportUnnecessaryTypeIgnoreComment = "warning"
個別ルールの強弱 disable_error_code / enable_error_code reportXxx = "none" / "warning" / "error"

既存コードへ段階的に導入する手順

一度に strict を有効にすると、指摘が数百件になって手が止まりがちです。「まず通す → 範囲を広げる → strictへ」の順で進めます。

段階 mypy pyright 目安
1. まず通す 既定設定+ignore_missing_imports。出たエラーは直すか # type: ignore[code] typeCheckingMode = "basic"、未解決importを整理 CIで exit 0 にする
2. 注釈なしの中身も見る check_untyped_defs = true standard(既定) 明らかな誤りが出る
3. 新規コードは厳しく overrides で新しいパッケージだけ disallow_untyped_defs = true strict = ["src/app/new_module"] 新規は注釈必須
4. 全体をstrictに strict = true+古い部分だけ overrides で緩める typeCheckingMode = "strict"+executionEnvironments で緩める 緩めた範囲を減らしていく
5. 抑止コメントを減らす warn_unused_ignores(strictに含む)で不要になったものを検出 reportUnnecessaryTypeIgnoreComment = "warning" 定期的に掃除

抑止コメントの書き方と「効いていないコメント」の検出

存在しない関数を呼ぶ行に、3通りのコメントを付けて両方にかけました。

from app import core


def demo() -> None:
    core.missing_func()  # 1) 存在しない関数(そのまま)
    core.missing_func()  # type: ignore[attr-defined]
    core.missing_func()  # pyright: ignore[reportAttributeAccessIssue]
    count: int = 1  # type: ignore[assignment]
    print(count)
$ mypy src/app/ignores.py    # strict なので warn_unused_ignores が有効
src/app/ignores.py:5: error: Module has no attribute "missing_func"  [attr-defined]
src/app/ignores.py:7: error: Module has no attribute "missing_func"  [attr-defined]
src/app/ignores.py:8: error: Unused "type: ignore" comment  [unused-ignore]
Found 3 errors in 1 file (checked 1 source file)

$ pyright src/app/ignores.py  # reportUnnecessaryTypeIgnoreComment を "warning" に設定
src/app/ignores.py
  src/app/ignores.py:5:10 - error: "missing_func" is not a known attribute of module "app.core" (reportAttributeAccessIssue)
  src/app/ignores.py:8:23 - warning: Unnecessary "# type: ignore" comment (reportUnnecessaryTypeIgnoreComment)
1 error, 1 warning, 0 informations
行 mypy pyright 読み方
5行目(コメントなし) error error 両方が検出
6行目 # type: ignore[attr-defined] 抑止 抑止 # type: ignore は両方に効く(pyrightは行のすべての指摘を抑止)
7行目 # pyright: ignore[...] error 抑止 pyright専用。mypyには効かない
8行目 不要な # type: ignore [unused-ignore] reportUnnecessaryTypeIgnoreComment 直したあとに残ったコメントを掃除できる

両方を使うチームでは # type: ignore[error-code] に統一すると、コメントが1つで済みます。エラーコードは mypy の出力末尾の [...]、ルール名は pyright の出力末尾の (...) をそのまま書きます。

pre-commit と GitHub Actions で自動実行する

pre-commit(ローカルで実行して確認)

pre-commit 4.6.2 で、mypyとpyrightの両方を走らせる設定です。mypyのフックは隔離された環境で動くため、必要なスタブは additional_dependencies に書きます。

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v2.3.1
    hooks:
      - id: mypy
        additional_dependencies: ["types-PyYAML"]   # 必要なスタブはここに
        args: ["--config-file=pyproject.toml"]
        pass_filenames: false                         # pyproject の files= を使う
  - repo: https://github.com/RobertCraigie/pyright-python
    rev: v1.1.414
    hooks:
      - id: pyright
$ pip install pre-commit
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

$ pre-commit run --all-files
[INFO] Installing environment for https://github.com/pre-commit/mirrors-mypy.
[INFO] Installing environment for https://github.com/RobertCraigie/pyright-python.
mypy.....................................................................Passed
pyright..................................................................Passed

find_task(tasks, 1).title(Noneの可能性がある戻り値をそのまま使う)を追加してコミットすると、両方のフックで止まりました。

$ git commit -m "add first_title"
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1

src/app/core.py:26: error: Item "None" of "Task | None" has no attribute "title"  [union-attr]
Found 1 error in 1 file (checked 6 source files)

pyright..................................................................Failed
- hook id: pyright
- exit code: 1

src/app/core.py
  src/app/core.py:26:32 - error: "title" is not a known attribute of "None" (reportOptionalMemberAccess)
1 error, 0 warnings, 0 informations

GitHub Actions(未実行:公式ドキュメントに沿った構成)

この記事の環境からはGitHubに接続できないため、次のワークフローは実行していません。actions/checkout v7、actions/setup-python v7 の書き方に沿っています。.mypy_cache をキャッシュすると2回目以降が大幅に短くなります。

# .github/workflows/typecheck.yml
name: typecheck
on:
  pull_request:
  push:
    branches: [main]

jobs:
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -e . mypy==2.3.1 types-PyYAML
      - uses: actions/cache@v4
        with:
          path: .mypy_cache
          key: mypy-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
      - run: mypy            # pyproject.toml の files= を使う

  pyright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install -e .
      - uses: jakebailey/pyright-action@v3   # Node.js を用意して pyright を実行
        with:
          version: 1.1.414

jakebailey/pyright-action を使わない場合は、actions/setup-node@v7 のあとに npm i -g pyright@1.1.414 && pyright でも同じです。

実行時間を実測する

2コアのLinuxコンテナで、同じコードを time で計測しました。結果は環境とコードの形に大きく左右されるので、「順序」ではなく「キャッシュの効き方」に注目してください。

対象 mypy 2.3.1 mypy(2回目、.mypy_cache あり) mypy --num-workers 2 pyright 1.1.414
sample.py 1ファイル 0.98秒 ― ― 1.16秒
生成した201ファイル・約5.8万行 3.9秒 0.15秒 2.5秒 12.3秒(--threads でも12.4秒)
rich 15.0.0(100ファイル・約2.6万行) 3.2秒 0.14秒 ― 13.9秒
$ time mypy rich
real    0m3.206s
$ time mypy rich          # 2回目
real    0m0.143s
$ time pyright rich
real    0m13.944s
$ pyright --stats rich | grep -E 'Parse|Bind|Check'
Parse:                0.53sec
Bind:                 0.63sec
Check:                9.95sec

pyrightの公式ドキュメントは「mypyより3〜5倍速い」と説明しており、多くのベンチマーク記事でもそう報告されていますが、この環境(2コア、mypycでコンパイル済みのmypy 2.3.1)ではmypyの方が速く、キャッシュありの2回目は0.15秒でした。pyrightの強みは、エディタ内で変更部分だけを再解析する言語サーバーとしての速さにあり、CLIの1回きりの実行では差が出にくいことがあります。CIでは mypyは .mypy_cache をキャッシュ、pyrightは差分ではなく全体を毎回と覚えておくと設計しやすくなります。

VS Code(Pylance)との関係と、スタブ(types-*)の入れ方

VS CodeのPython補完や赤線を出しているPylanceは、pyrightをエンジンにした拡張機能です。設定 python.analysis.typeCheckingMode の既定は off(エラー表示なし)で、basic / standard / strict に上げるとCLIのpyrightと同じ指摘が出ます。プロジェクトに pyrightconfig.json や [tool.pyright] があるとPylanceもそれを読むので、設定をファイルに置けばエディタとCIの結果が揃います。mypyをエディタで使うときはMicrosoft製の拡張「Mypy Type Checker」(ms-python.mypy-type-checker)を入れると、保存時にmypyの指摘が表示されます。

mypy --install-types でスタブを入れる

PyYAMLのように「ライブラリ本体に型情報がなく、typeshedに types-* パッケージがある」ものは、mypyがヒントを出します。

$ mypy load_config.py
load_config.py:1: error: Library stubs not installed for "yaml"  [import-untyped]
load_config.py:1: note: Hint: "python3 -m pip install types-PyYAML"
load_config.py:1: note: (or run "mypy --install-types" to install all missing stub packages)
Found 1 error in 1 file (checked 1 source file)

$ mypy --install-types --non-interactive load_config.py
Installing missing stub packages:
.venv/bin/python3 -m pip install types-PyYAML
Successfully installed types-PyYAML-6.0.12.20260906

Success: no issues found in 1 source file

pyrightは同じファイルで 0 errors でした。pyrightはスタブがなくてもライブラリのソースを読んで推論するため、standardでは何も言わず、strictでは reportMissingTypeStubs として指摘します(types-* が無いライブラリの例)。

$ pyright use_toolz.py    # typeCheckingMode: strict
  use_toolz.py:1:6 - error: Stub file not found for "toolz" (reportMissingTypeStubs)
  use_toolz.py:1:19 - error: Type of "groupby" is partially unknown
    Type of "groupby" is "(key: Unknown, seq: Unknown) -> dict[Unknown, Unknown]" (reportUnknownVariableType)
状況 mypyの出力 対処
typeshedに types-* がある Library stubs not installed for "yaml" [import-untyped] pip install types-PyYAML または mypy --install-types
本体はあるが py.typed もスタブもない module is installed, but missing library stubs or py.typed marker [import-untyped] overrides で ignore_missing_imports = true、または自作スタブを mypy_path に置く
そもそも見つからない Cannot find implementation or library stub for module named "toolz" [import-not-found] 仮想環境が違う。次の節へ

よくあるつまずきと対処

症状(出力) ツール 原因 対処
Module has no attribute "missing_func" [attr-defined] mypy 存在しない属性、または動的に生えるためスタブにない 名前を確認。動的属性なら getattr か # type: ignore[attr-defined]
"missing_func" is not a known attribute of module "app.core" (reportAttributeAccessIssue) pyright 同上 同上(# pyright: ignore[reportAttributeAccessIssue])
Import "toolz" could not be resolved (reportMissingImports) pyright pyrightが見ているPythonが仮想環境と違う 仮想環境を有効にして実行、または venvPath/venv、--pythonpath .venv/bin/python
Cannot find implementation or library stub for module named "toolz" [import-not-found] mypy 仮想環境違い、または .mypy_cache が古い python_executable を指定。パッケージを入れた直後なら rm -rf .mypy_cache
Library stubs not installed for "yaml" mypy types-* 未インストール mypy --install-types --non-interactive
Stub file not found for "toolz" (reportMissingTypeStubs) pyright(strict) スタブがない reportMissingTypeStubs = "none" か pyright --createstub toolz で雛形生成
Unused "type: ignore" comment [unused-ignore] mypy 直したあとにコメントが残っている コメントを消す
Invalid "type: ignore" comment [syntax] mypy # type: ignore[code] のあとに別の文字を続けた コメントは # type: ignore[code] で終える(説明は別行に)
venv .venv subdirectory not found in venv path ... pyright venvPath と venv の組み合わせが実在しない venvPath は「仮想環境の親」、venv は「フォルダ名」
同じファイルでエディタとCIの結果が違う 両方 エディタは off/別の設定を読んでいる 設定を pyproject.toml に一本化し、Pylanceの typeCheckingMode を合わせる

仮想環境違いは実際に再現できます。toolz を .venv にだけ入れた状態で、仮想環境を有効にせずに実行した出力です。

$ .venv/bin/pyright use_toolz.py        # PATH の python が仮想環境の外
use_toolz.py
  use_toolz.py:1:6 - error: Import "toolz" could not be resolved (reportMissingImports)
1 error, 0 warnings, 0 informations

$ . .venv/bin/activate && pyright use_toolz.py
0 errors, 0 warnings, 0 informations

mypyでは、パッケージを入れる前の結果が .mypy_cache に残っていたために import-not-found が出続け、rm -rf .mypy_cache で import-untyped(本来の指摘)に変わりました。「入れたのに見つからないと言われる」ときは、まずキャッシュを疑うと早く解決します。

新しいチェッカー:ty と pyrefly

2025年以降、Rustで書かれた新しい型チェッカーが登場しています。ty(Astral社。ruff/uvの開発元。PyPI 0.0.84、プレビュー段階)と pyrefly(Meta社。PyPI 1.3.2)で、どちらも pip install ty / pip install pyrefly で入り、ty check / pyrefly check で実行します。同じrichのコードを同じ環境で計ると、tyは0.73秒、pyreflyは0.71秒でした(mypy 3.2秒、pyright 13.9秒)。ただし検出内容や設定項目はまだ変化が大きく、ライブラリ側の対応もこれからなので、現時点では「mypyかpyrightを本線にして、速度が必要なところで試す」位置づけが現実的です。

よくある質問

Q. mypyとpyrightは両方入れるべきですか? A. 必須ではありません。エディタで使っているもの(VS Codeならpyright)を本線にして、1つに揃えるのが基本です。ライブラリを公開する場合や、両方のユーザーに配慮したい場合に、CIで両方を回す価値が出てきます。この記事の .pre-commit-config.yaml はそのまま両方を実行できます。

Q. Any と Unknown は何が違いますか? A. Any は「何でも受け入れる」と明示した型で、両方のツールでチェックを通します。Unknown はpyrightだけの呼び方で、注釈がなくて型が決められなかった「暗黙のAny」を指します。strictモードでは Unknown が残ること自体が指摘されるため、注釈を足す場所がわかります。mypyはこの区別をしません。

Q. # type: ignore は最終手段ですか? A. 段階的な導入では積極的に使って構いません。必ずエラーコードを付け(# type: ignore[union-attr])、warn_unused_ignores で不要になったものを検出する運用にすると、あとから減らしていけます。コード全体を対象から外す exclude よりも、行単位の方が見通しがよくなります。

Q. Python 3.9のコードもチェックできますか? A. mypy 2.0以降は --python-version 3.9 をサポートしていません(3.10以上)。3.9向けにチェックしたい場合は mypy 1.20系を使うか、pyrightの pythonVersion = "3.9" を使います。

まとめ

  • mypyは「型を1つに決める」ことに厳格、pyrightは「注釈のないコードも推論する」ことに強い。同じサンプルで既定の検出数は7件と9件、strictでは14件と21件
  • 分岐後の型は mypyがjoin(object)、pyrightがunion(str | int)。reveal_type() で確かめられる
  • 設定は pyproject.toml の [tool.mypy] と [tool.pyright] に集約し、エディタとCIで同じファイルを読ませる
  • 段階導入は 「通す → 注釈なしの中身も見る → 新規だけstrict → 全体strict」。抑止は # type: ignore[code] に統一
  • pre-commitに mirrors-mypy と pyright-python を入れると、型エラーのあるコミットはローカルで止まる(実行結果つき)
  • 実行時間はこの環境で mypy 3.2秒(キャッシュあり0.14秒)、pyright 13.9秒。CIでは .mypy_cache をキャッシュする
  • VS CodeのPylanceはpyright。既定の typeCheckingMode は off なので、standard 以上に上げると指摘が見える

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「mypyとpyrightの違いと使い方|設定例・CI導入・出力比較」

株式会社RJCでは、型チェッカーのようなコードの品質を支える道具を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?