型ヒントを覚えて、関数に price: int と書いてみた。ところが文字列を渡しても、Pythonは何も言わずに動いてしまった。
そんな肩透かしを食らった経験はありませんか。
実はこれ、バグではなくPythonの仕様どおりの動きです。型ヒントはあくまで注釈であって、実行時に中身を確かめる仕組みではありません。
その注釈を本物のチェックに変えてくれるのが mypy です。この記事では、mypyが何をしてくれるのか、どこから使い始めればいいのかを、手元で動かせる形で順番に見ていきます。
型ヒントは、書いただけでは誰もチェックしてくれない¶
まずは、いま自分のコードで何が起きているのかを確かめましょう。次のコードは、金額に消費税を足して返すだけの関数です。
def add_tax(price: int) -> int:
return int(price * 1.1)
print(add_tax("1000"))
price: int と書いてあるのに、呼び出しでは文字列を渡しています。これを実行すると、どこで止まると思いますか。
答えは、型ヒントの行では止まらない、です。実行は普通に始まって、掛け算のところで TypeError になります。
つまりPythonは、型ヒントを見て呼び出しを止めてはくれません。エラーが出たのはたまたま、文字列と小数を掛けられなかったからです。
もし関数の中身が文字列でも動いてしまう処理だったら、誰も気づかないまま間違った値が流れていきます。型ヒントは実行時の検査ではなく、人と道具のために置かれた注釈です。
型ヒントそのものの書き方に不安がある方は、先に【関連記事】Pythonの型ヒントとは?型ヒントの基礎を解説へ目を通しておくと、この先がぐっと読みやすくなります。
mypyは、動かさずに型の食い違いを見つける道具¶
ここで登場するのが mypy です。コードを実行せずに読み込んで、型ヒントと実際の使われ方が食い違っている場所を教えてくれます。
こうした道具は静的型チェッカーと呼ばれます。静的とは、プログラムを動かさずに調べるという意味です。
テストが実際に走らせて結果を見るのに対して、mypyは走らせる前に読んで確かめます。この違いが、あとで効いてきます。
インストールして、まず1ファイルだけ試す¶
導入はとても簡単で、pipで入れるだけです。プロジェクトごとの仮想環境の中に入れるのがおすすめです。
pip install mypy
mypy add_tax.py
さきほどのファイルにmypyをかけると、次の1行が返ってきます。
add_tax.py:4: error: Argument 1 to "add_tax" has incompatible type "str"; expected "int" [arg-type]
4行目の第1引数が str になっているけれど、期待しているのは int だ、という指摘です。実行しなくても書いた時点で分かる、というのが大きな違いです。
指摘が1つも無ければ、Success: no issues found in 1 source file と表示されます。ここを目指して直していく形になります。
エラーメッセージは、いつも同じ並びで出てくる¶
mypyの出力は形が決まっているので、慣れると一瞬で読めるようになります。左から順に、場所、種類、説明、そして角かっこのエラーコードです。
さきほどの1行を、部分ごとに分けて見てみましょう。
| 部分 | さきほどの例 | 意味 |
|---|---|---|
| 場所 | add_tax.py:4 |
どのファイルの何行目か |
| 種類 | error |
直すべき指摘。note は補足の説明 |
| 説明 | has incompatible type "str"; expected "int" |
何と何が食い違っているか |
| エラーコード | [arg-type] |
指摘の分類。検索するときの手がかりになる |
初心者のうちは、まずエラーコードだけ拾い読みするのがおすすめです。よく出るものはそれほど多くありません。
| エラーコード | どんなとき出るか |
|---|---|
arg-type |
関数に渡した引数の型が合っていない |
assignment |
変数に宣言と違う型を入れた |
attr-defined |
そのオブジェクトに無い属性を触っている |
union-attr |
None かもしれない値をそのまま使っている |
no-untyped-def |
関数に型ヒントが付いていない |
attr-defined は、属性名のタイプミスでもよく出ます。self.name と書くところを u.nama と打っただけで、動かす前に気づけるわけです。
いちばん効くのは、Noneのすり抜けを止められること¶
私が実務でmypyに助けられた場面は、型の取り違えよりも None がらみのほうが圧倒的に多いです。
代表的なのが、辞書の get() を使ったときです。キーが無ければ None が返るので、戻り値は文字列とNoneのどちらにもなります。
def find_user(user_id: int) -> str | None:
users = {1: "sato", 2: "suzuki"}
return users.get(user_id)
name = find_user(3)
print(name.upper())
このコードを実行すると、AttributeError: 'NoneType' object has no attribute 'upper' で落ちます。存在しないIDを引くケースを試さない限り、気づけません。
ところがmypyをかけると、動かす前に次のように教えてくれます。
find_user.py:6: error: Item "None" of "str | None" has no attribute "upper" [union-attr]
str と None のうち None のほうに upper は無い、という指摘です。if name is not None: の分岐を足せば、この指摘は消えます。
私は10年ほどエンジニアとして開発に関わってきましたが、本番で起きた障害の原因をたどると、想定していなかった None にたどり着くことが本当に多いです。データが必ずあると思い込んでいた場所に、たまたま無い日があった、という類いの事故です。
mypyは、その手のすり抜けをかなりの割合で先に潰してくれます。None という値の性質そのものが気になった方は、【関連記事】Noneはただの空ではない。Pythonに1つしか存在しないシングルトンの正体とは?もあわせてどうぞ。
型を書いていない関数は、初期設定では見逃される¶
ここで、初めてmypyを使った人がだいたい戸惑う点にふれておきます。
型ヒントを1つも書いていない関数は、既定では中身を調べてもらえません。
def greet(name):
return "こんにちは、" + name
greet(123)
このファイルにmypyをかけると、Success: no issues found と出ます。文字列と数値を足そうとしているのに、何も言われません。
理由は、mypyが少しずつ型を足していく使い方を前提に作られているからです。注釈の無い関数まで最初から全部怒られると、動いている既存のコードには入れられなくなってしまいます。
厳しく調べてほしいときは --strict を付けます。同じファイルが、今度は2件の指摘に変わります。
greet.py:1: error: Function is missing a type annotation [no-untyped-def]
greet.py:4: error: Call to untyped function "greet" in typed context [no-untyped-call]
既定と --strict では、見てくれる範囲がこれだけ変わります。
| 状況 | 既定 | --strict |
|---|---|---|
| 型ヒント付きの関数の中身 | 調べる | 調べる |
| 型ヒントの無い関数の中身 | 調べない | 調べる |
| 型ヒントの無い関数の呼び出し | 何も言わない | 指摘する |
| 導入のしやすさ | 入れた初日から静か | 既存コードだと指摘が大量に出る |
新しく作るプロジェクトなら、最初から --strict で始めるのが結局いちばん楽です。すでに動いているコードへ後から入れるなら、既定から始めて少しずつ締めていきましょう。
設定はpyproject.tomlに書いて、チーム全員で揃える¶
毎回オプションを手で打つのは、現実的ではありません。設定ファイルに書いておけば、mypy . と打つだけで誰の手元でも同じ条件になります。
いまのPythonでは pyproject.toml にまとめるのが主流です。[tool.mypy] という見出しの下へ、次のように書きます。
[tool.mypy]
python_version = "3.13"
disallow_untyped_defs = true
warn_return_any = true
disallow_untyped_defs は、型ヒントの無い関数を指摘する設定です。--strict はこうした設定をまとめて有効にするスイッチなので、慣れてきたら1つずつ選んで足していくほうが調整しやすくなります。
この書き方の利点は、設定がリポジトリに残ることです。手元でもレビューでもCIでも、同じ基準でコードを見られる状態を作るのが目的です。
設定ファイルそのものについては、【関連記事】pyproject.tomlとは?これからのPythonプロジェクト管理入門で詳しく扱っています。
Ruffやpytestとは、担当している場所が違う¶
ここまで読んで、Ruffやテストとの違いが気になった方もいるはずです。同じような場所に置かれる道具ですが、見ているものはきれいに分かれています。
3つの担当を並べると、関係がつかみやすいと思います。
| 道具 | 見ているもの | 見てくれないこと |
|---|---|---|
| Ruff | 書き方の乱れ、未使用の変数、import の並び | 型の食い違い |
| mypy | 型ヒントと実際の使われ方の食い違い | 計算結果が正しいかどうか |
| pytest | 実際に動かしたときの結果 | 一度も通っていない分岐 |
この3つは競合しません。重なっていないからこそ、全部そろえたときに安心できるわけです。
Ruffをまだ触っていない方は【関連記事】Ruff(ラフ)とは?Pythonで綺麗なコードを書く初心者向けにわかりやすく解説を、自動で回す仕組みが気になる方は【関連記事】GitHub Actions入門。Pythonコードのテストから公開までを自動化しようをどうぞ。
もうひとつ、経験から言えることがあります。mypyは入れた初日がいちばんつらいです。
数年動いてきたコードにいきなりかけると、指摘が数百件出ることも珍しくありません。そこで心が折れて設定ごと外してしまったチームを、私は何度か見てきました。
うまくいったのは、新しく書くファイルだけを対象にして、触った場所から順に広げていくやり方でした。全部を一度にきれいにしようとしないのが、続けるコツです。
いまのmypyと、始めるときの現実的な順番¶
最後に、道具そのものの現在地にふれておきます。
PyPIで配布されている最新版は 2.3.1 で、2026年8月15日に公開されました。動かすにはPython 3.10以降が必要です。
メジャーバージョンが2に上がったのは2026年で、いくつかの既定値が厳しい側へ変わっています。既存のプロジェクトを上げるときは、公式の変更履歴に目を通しておくと安心です。
最初の1週間でやることは、これだけでいい¶
たくさん設定があると身構えてしまいますが、はじめの一歩はとても小さくて大丈夫です。
まず、いちばん自信のないファイル1つに mypy をかけて、出力の形に慣れる。次に、自分がこれから書く関数にだけ型ヒントを足していく。
そのあとで pyproject.toml に設定を置き、最後にCIへ組み込む。この順番なら、途中で嫌になりにくいはずです。
mypyは、あとから足せる道具です。今日書くファイルから始めれば、それで十分に元は取れます。
まとめ¶
mypyは、型ヒントをただの注釈から本物のチェックへ変えてくれる道具です。実行しなくても、型の食い違いや None のすり抜けを先に見つけられます。
既定では型ヒントの無い関数を見逃すので、慣れてきたら --strict や pyproject.toml の設定で少しずつ締めていきましょう。大事なのは、完璧にすることではなく、続けられる強さで始めることです。
Ruffは書き方、pytestは動かした結果と、担当がきれいに分かれています。どれかがどれかの代わりになることはなく、3つそろって、はじめて守りが厚くなります。
まずは手元のファイル1つに mypy をかけてみてください。そこで出てきた1行は、あなたのコードが次に落ちるはずだった場所かもしれません。
ここまでお読みいただきありがとうございました。