- 技術 💻
mypyとpyrightの違いと使い方|設定例・CI導入・出力比較
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期連続の黒字決算。