変数、基本型と型ヒント
Python は動的言語ですが、モダンPythonでは最初から型を書きます。int/str/bool/None と組み込みジェネリクス、int | None の短縮記法、mypy/pyright までまとめて整理します。
第1章 はじめ方と uv セットアップ で作ったプロジェクトの上に、そのまま続きます。本章のテーマは 変数と基本型、そして型ヒント です。
本書の中心原則の一つが「型ヒント優先」だという点は、序文で述べました。この原則が実際のコード上でどう現れるのかを、本章で初めて目にします。第9章で扱う Protocol や第20章の ParamSpec のような高度な型ツールも、結局は本章の基礎の上に積み上げるものです。ここを丁寧に読んでおくと、後がぐっと軽くなります。
Python は動的型付け言語です。変数に型を書く必要はありません。それでも モダンPythonでは最初から型を書く という流れが標準になりました。なぜそうするのか、どう書くのかを整理していきます。
変数 — 宣言は別にない #
他の言語との大きな違いの一つです。let、var、int x のようなキーワードはありません。名前に値を代入すれば、それが変数です。
name = "curtis"
age = 30
height = 175.5
is_admin = True名前は snake_case が慣習です。JavaScript の camelCase と違うため、最初は違和感があります。定数として扱う値は大文字 + アンダースコア (MAX_RETRY = 3) です。
基本型 — 4 + 1 #
最もよく使う 5 つから押さえれば十分です。
i: int = 42
f: float = 3.14
s: str = "hello"
b: bool = True
n: None = Noneここの : int の部分が 型ヒント です。変数名の後にコロンと型を書きます。動作には影響しませんが、ツール(IDE / 型チェッカ) が読んで検証や補完を行ってくれます。
動的型付けなのに、なぜ型を書くのか #
型ヒントは ランタイムでは無視されます。 次のコードは正常に実行されます。
x: int = "this is a string"
print(x) # this is a stringそれでも書く理由は次の通りです。
- エディタの補完と定義ジャンプが正確になる — VS Code、PyCharm のどちらも型を見ている
- 型チェッカ(mypy / pyright / Pyrefly) がコンパイル時に問題を捕まえてくれる
- 他人がコードを読むとき の文書になる — 関数シグネチャだけで何を受けて何を返すか分かる
- リファクタリングの安全網 — 関数シグネチャを変えるとどこが壊れるか見える
古いPythonコードには型が付いていません。新しいコードでは、可能な限り型を書くのがモダンPythonの原則です。本書もその原則に従います。
型ヒント — 変数、関数、コレクション #
変数 #
count: int = 0
name: str
name = "curtis" # 宣言だけ先に、値は後でname: str のように値なしで型だけ書くこともできます。クラスフィールド宣言でよく使います(第8章 dataclass で本格的に登場します)。
関数 #
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> None:
print(f"hi, {name}")引数の後に : 型、戻り値は -> 型 です。戻り値がなければ -> None です。これを書かない関数は IDE 側が「何を返すか分からない」状態になり、その関数を使う側のコードの補完も同じく弱くなります。
コレクション — 組み込みジェネリクス #
リスト、辞書のようなコレクションは 要素の型まで一緒に書きます。
nums: list[int] = [1, 2, 3]
names: list[str] = ["a", "b"]
ages: dict[str, int] = {"curtis": 30, "smith": 25}
unique: set[str] = {"a", "b"}
point: tuple[float, float] = (1.0, 2.0)list[int] のように組み込み型へそのまま [] を付ける記法 は Python 3.9 から使えます。それ以前は from typing import List をしてから List[int] でした。古いコードにはまだ見かけますが、新しいコードでは組み込み側に統一すれば十分です。
from typing import List, Dict
nums: List[int] = [1, 2, 3]
ages: Dict[str, int] = {"curtis": 30}付録Aの 旧Pythonコードを modern スタイルに移す では、この変換がツールで自動化可能なことを改めて扱います。
None とオプション型 — int | None
#
値があるかもしれないし、ないかもしれない場合を表現するとき。
def find_user(id: int) -> str | None:
if id == 1:
return "curtis"
return Nonestr | None は Python 3.10 から使える union の短縮記法 です。それ以前は Optional[str] または Union[str, None] を typing から import する必要がありました。
from typing import Optional, Union
def find_user(id: int) -> Optional[str]: ...
def parse(value: str) -> Union[int, float, None]: ...新規コードは無条件に | です。短く、追加の import が要りません。
def parse(value: str) -> int | float | None:
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return None数値 — int と float #
Python の int は 任意精度 です。64bit の限界がありません。
big = 10 ** 100
print(big)
# 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000float は IEEE 754 double です。他の言語と同じく、浮動小数点の落とし穴がそのまま存在します。
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False金融や正確な小数が必要なら decimal.Decimal を使います。
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3数値リテラルの小さなコツ #
読みやすさのためにアンダースコアを挟めます (3.6+)。
ONE_MILLION = 1_000_000
HEX = 0xFF_FF
BIN = 0b_1010_1010文字列 — str と f-string
#
シングルクォート / ダブルクォートに動作の差はありません。プロジェクト内で一貫していれば十分です (通常は ")。
s1 = "hello"
s2 = 'world'
s3 = """複数行の
文字列"""値を埋め込むときは f-string を使います。
name = "curtis"
age = 30
print(f"hi, {name}! you are {age} years old.")
print(f"next year you'll be {age + 1}.")
print(f"{name = }") # name = 'curtis' (デバッグに便利)f"{変数 = }" の形は 変数名と値を一緒に出力 します。デバッグ用 print によく使われます。
t-string — Python 3.14 の新文法 #
Python 3.14 で PEP 750 によって t-string が追加されました。f-string と見た目は似ていますが、補間が即時には行われず Template オブジェクトとして残る ところが違いです。
from string.templatelib import Template
name = "curtis"
tpl: Template = t"hi, {name}!"
# Template オブジェクト。まだ文字列に合成されていない状態。
# 安全に処理する関数に渡す
def render_html(template: Template) -> str: ...
html = render_html(t"<a href='{user_url}'>{user_name}</a>")いつ使うのか。ユーザー入力を安全に処理する必要がある場面 — SQL、HTML、shell です。f-string は補間結果がすぐ文字列になるためエスケープを忘れがちですが、t-string ではライブラリ側がエスケープの責任を担えます。入門段階では「f-string が分かれば十分」で、ライブラリコードで t-string を見かけたら、それがそれだと認識できればよいです。
bool と真偽値
#
True、False の 2 つで、bool は int のサブタイプ です。
print(True + True) # 2
print(isinstance(True, int)) # True興味深い性質ですが、bool の位置に int を混ぜて使うのは推奨されません。明示的に True / False を使ってください。
真偽値 (truthy / falsy) #
空のコンテナと 0 は偽、それ以外は真です。
if not []: print("empty list is falsy")
if not "": print("empty str is falsy")
if not 0: print("zero is falsy")
if not None: print("None is falsy")これを利用して if not items: のような表現がよく使われます。JavaScript の if (!arr.length) よりも短いです。
型変換 — 明示的に #
Python は 暗黙の型変換をほとんど行いません。 整数と文字列を足すとエラーになります。
n = 42
s = "answer: " + str(n) # str(n) で変換する必要あり
# "answer: " + n → TypeError
age = int(input("年齢: ")) # input は str を返す → int に変換よく使う変換は次の通りです。
str(42) # '42'
int("42") # 42
int("42", 16) # 66 (16進数として解釈)
float("3.14") # 3.14
bool(0) # False
bool("any") # True (空文字列だけ False)
list("abc") # ['a', 'b', 'c']型エイリアス — type キーワード
#
同じ形の型が何度も出てくるなら、名前を付けます。
type UserId = int
type UserName = str
type UserMap = dict[UserId, UserName]
def get_user(id: UserId) -> UserName | None: ...type 構文は Python 3.12 で追加されました。それ以前は次のように書いていました。
UserId = int # エイリアスのように働く変数
# または
from typing import TypeAlias
UserId: TypeAlias = int新規コードは type に統一すれば十分です。
型チェッカ — mypy / pyright #
型ヒントはランタイムでは検証されないため、別のツールが静的検証を行います。 2 つの陣営があります。
- mypy — もっとも古い標準。Python 陣営のツール。
uv add --dev mypy - pyright / Pylance — Microsoft 製。高速で VS Code との統合が良い。
uv add --dev pyright - Pyrefly — Astral の新作 (2026 現在ベータ) 。さらに速い次世代候補
新規プロジェクトは pyright から始めるのが無難です。VS Code では Python 拡張を入れれば Pylance として自動で動くので、別途の設定もほとんど不要です。
uv add --dev pyright
uv run pyright .小さな例で動作を見ると次の通りです。
def add(a: int, b: int) -> int:
return a + b
result = add("hello", 1) # ✗ str は int ではない$ uv run pyright check.py
check.py:4:18 - error: Argument of type "Literal['hello']" cannot be assigned to parameter "a" of type "int"ランタイムの前に捕まえます。最初は小言のように感じるかもしれませんが、一ヶ月も使うと「これなしでよくやっていたな」と思うようになります。
本章では pyright を一行コマンドで呼んだだけですが、第30章 型チェッカ設定と CI 統合 で pyproject.toml 設定・strict オプション・pre-commit・GitHub Actions まで本式の運用セットアップを整えます。
練習問題 #
本章の核心は「型を書けばツールが捕まえてくれる」です。直接ツールが捕まえる様子を見てください。
- 新しいプロジェクト
typing-playを作り、pyrightを dev 依存として追加してください。play.pyにdef add(a: int, b: int) -> int: return a + bを書き、わざとadd("hello", 1)を呼び出した後、uv run pyright play.pyがエラーを捕まえることを確認してください。 play.pyにdef first(items: list[int]) -> int | None: return items[0] if items else Noneを定義し、first([1, 2, 3])とfirst([])の両方が動作するか確認してください。戻り値の型がint | Noneなので、呼び出し側でresult + 1のようなコードを書くと pyright がNoneの可能性を指摘します。そのエラーも直接見てください。typeキーワードでtype UserId = intとtype UserMap = dict[UserId, str]を定義し、UserMapを引数に受け取る関数を一つ書いてください。pyright がUserIdとintを相互に互換と認識するか (3.12+ のtype構文は単純なエイリアスです) 確認します。
一行まとめ: モダンPythonでは変数 / 関数 / コレクション / オプションのすべてに型を書く。組み込みジェネリクスは
list[int]、オプションはT | None、エイリアスはtype Name = ...、ツールは pyright。ランタイム動作には影響しないが、IDE ・ 型チェッカ ・ リファクタリングがすべて変わる。
次の章 #
次の 第3章 制御フロー — if, while, for, match-case では、流れの制御 — if、while、for、そして 3.10 で入った match-case を扱います。他の言語の switch と性格がどう違うかが核心です。本章で押さえた型ヒントは、第3章の match-case のパターン分岐でもう一度活用されます。