目次
2 章

変数、基本型と型ヒント

Python は動的言語ですが、モダンPythonでは最初から型を書きます。int/str/bool/None と組み込みジェネリクス、int | None の短縮記法、mypy/pyright までまとめて整理します。

第1章 はじめ方と uv セットアップ で作ったプロジェクトの上に、そのまま続きます。本章のテーマは 変数と基本型、そして型ヒント です。

本書の中心原則の一つが「型ヒント優先」だという点は、序文で述べました。この原則が実際のコード上でどう現れるのかを、本章で初めて目にします。第9章で扱う Protocol や第20章の ParamSpec のような高度な型ツールも、結局は本章の基礎の上に積み上げるものです。ここを丁寧に読んでおくと、後がぐっと軽くなります。

Python は動的型付け言語です。変数に型を書く必要はありません。それでも モダンPythonでは最初から型を書く という流れが標準になりました。なぜそうするのか、どう書くのかを整理していきます。

変数 — 宣言は別にない #

他の言語との大きな違いの一つです。letvarint 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

それでも書く理由は次の通りです。

  1. エディタの補完と定義ジャンプが正確になる — VS Code、PyCharm のどちらも型を見ている
  2. 型チェッカ(mypy / pyright / Pyrefly) がコンパイル時に問題を捕まえてくれる
  3. 他人がコードを読むとき の文書になる — 関数シグネチャだけで何を受けて何を返すか分かる
  4. リファクタリングの安全網 — 関数シグネチャを変えるとどこが壊れるか見える

古い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] でした。古いコードにはまだ見かけますが、新しいコードでは組み込み側に統一すれば十分です。

古い方式 (3.8 以下) — 新規コードでは使わない
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 None

str | 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)
# 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

float は 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 を使います。

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 オブジェクトとして残る ところが違いです。

t-string (3.14+)
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 と真偽値 #

TrueFalse の 2 つで、boolint のサブタイプ です。

bool は int
print(True + True)     # 2
print(isinstance(True, int))  # True

興味深い性質ですが、bool の位置に int を混ぜて使うのは推奨されません。明示的に True / False を使ってください。

真偽値 (truthy / falsy) #

空のコンテナと 0 は偽、それ以外は真です。

falsy 値
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 キーワード #

同じ形の型が何度も出てくるなら、名前を付けます。

型エイリアス (3.12+)
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 として自動で動くので、別途の設定もほとんど不要です。

pyright を追加
uv add --dev pyright
uv run pyright .

小さな例で動作を見ると次の通りです。

check.py
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 まで本式の運用セットアップを整えます。

練習問題 #

本章の核心は「型を書けばツールが捕まえてくれる」です。直接ツールが捕まえる様子を見てください。

  1. 新しいプロジェクト typing-play を作り、pyright を dev 依存として追加してください。play.pydef add(a: int, b: int) -> int: return a + b を書き、わざと add("hello", 1) を呼び出した後、uv run pyright play.py がエラーを捕まえることを確認してください。
  2. play.pydef first(items: list[int]) -> int | None: return items[0] if items else None を定義し、first([1, 2, 3])first([]) の両方が動作するか確認してください。戻り値の型が int | None なので、呼び出し側で result + 1 のようなコードを書くと pyright が None の可能性を指摘します。そのエラーも直接見てください。
  3. type キーワードで type UserId = inttype UserMap = dict[UserId, str] を定義し、UserMap を引数に受け取る関数を一つ書いてください。pyright が UserIdint を相互に互換と認識するか (3.12+ の type 構文は単純なエイリアスです) 確認します。

一行まとめ: モダンPythonでは変数 / 関数 / コレクション / オプションのすべてに型を書く。組み込みジェネリクスは list[int]、オプションは T | None、エイリアスは type Name = ...、ツールは pyright。ランタイム動作には影響しないが、IDE ・ 型チェッカ ・ リファクタリングがすべて変わる。

次の章 #

次の 第3章 制御フロー — if, while, for, match-case では、流れの制御 — ifwhilefor、そして 3.10 で入った match-case を扱います。他の言語の switch と性格がどう違うかが核心です。本章で押さえた型ヒントは、第3章の match-case のパターン分岐でもう一度活用されます。

X