# Python のデータ型 完全ガイド：数値・文字列・コレクションの『正しい使い分け』と本番で壊れない設計

> Pythonの組み込みデータ型（int / float / Decimal・str・bool・None・list / tuple / dict / set）を、CPythonの内部構造・可変性・計算量から本番設計まで体系化。float誤差とお金の扱い、ミュータブルデフォルト引数、is と ==、型ヒント、Pydantic / marshmallow による境界検証まで、実プロジェクトの実務知見で『使い分けの判断軸』として解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Python, 型安全, アーキテクチャ設計, パフォーマンス, Pydantic, marshmallow
- URL: https://tomodahinata.com/blog/python-data-types-complete-guide
- カテゴリ: Pythonバックエンド
- 総合ガイド: https://tomodahinata.com/blog/fastapi-production-async-pydantic-observability-guide

## 要点

- Pythonでは『すべてがオブジェクト』で、変数は値ではなく参照を束ねる——この一点を理解すると、可変性・is と ==・コピーにまつわるバグが体系的に説明できる
- float は IEEE 754 の2進浮動小数点で 0.1 + 0.2 は 0.3 にならない。お金は必ず Decimal か整数の最小単位で持つ（本番二重課金0件の決済基盤で徹底した原則）
- list / tuple / dict / set は『可変か・順序があるか・ハッシュ可能か・計算量はいくつか』で選ぶ。メンバーシップ判定は list（O(n)）ではなく set / dict（O(1)）
- ミュータブルデフォルト引数・共有参照・浅いコピーは、動的型 Python で最も多い本番バグ源。None センチネルと不変型・deepcopy で構造的に防ぐ
- 型ヒント＋mypy / pyright、dataclass / Enum / TypedDict で『不正な状態を表現不能』にし、システム境界は Pydantic / marshmallow で検証する——動的言語に静的な安全を与える

---

## **導入：データ型は「暗記する一覧」ではなく「設計の語彙」である**

「Python のデータ型」と検索すると、たいてい `int` / `float` / `str` / `list` / `dict` …という**一覧表**にたどり着きます。それは出発点としては正しい。しかし、現場で人を分けるのは「どんな型があるか」を知っているかではなく、**「いつ・なぜ・どの型を選ぶか」を判断軸として持っているか**です。

なぜ金額計算に `float` を使うと本番事故になるのか。なぜ `list` への `in` 判定がスケールしないのか。なぜ「初期値に空リストを渡した関数」が、呼ぶたびに過去のデータを引きずるのか。これらはすべて、**データ型の「内部の挙動」を理解していれば未然に防げる**バグです。逆に言えば、ここを曖昧にしたまま動的型の Python を書くと、テストをすり抜けて本番でだけ壊れます。

本記事は、世界中で読まれている [Real Python の "Basic Data Types in Python"](https://realpython.com/python-data-types/) が扱う範囲——数値・文字列・真偽値・None・コレクション——を**土台にしつつ**、そこから先の「本番で効く実装知」まで一気通貫で踏み込みます。具体的には、

- CPython の**オブジェクトモデル**（なぜ `is` と `==` を混同すると痛い目を見るのか）
- `float` の**IEEE 754 の罠**と、お金を扱うときの正解（`Decimal` / 整数の最小単位）
- コレクションの**計算量（Big-O）**に基づく使い分け
- 動的型に**静的な安全**を与える型ヒントと、`dataclass` / `Enum` / `TypedDict` による「型の設計」
- **システム境界での検証**（Pydantic / marshmallow）

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・実装し、また**本番二重課金0件**を達成したサーバーレス決済プラットフォームで決済信頼性レイヤーを主導してきました。本記事の随所に出てくる「お金は `float` で持たない」「不正な状態を表現不能にする」といった原則は、すべてその実戦から得たものです。単なる文法の翻訳ではなく、**発注して安心して任せられるレベルの設計判断**として読んでいただけるよう書きました。

> 💡 **対象バージョン**：本記事は Python **3.12 / 3.13** を前提とします（記述の大半は 3.10 以降で有効）。バージョンに依存する機能には、導入されたバージョンを明記します。

---

## **0. すべての出発点：Python では「すべてがオブジェクト」である**

データ型の話を「型の一覧」から始めると、必ずどこかで詰まります。正しい出発点は、**Python のオブジェクトモデル**です。ここを 5 分で押さえれば、この後の可変性・コピー・`is`/`==` の話がすべて一本の線でつながります。

Python では、整数も文字列も関数もクラスも、**すべてが「オブジェクト」**です。そして、すべてのオブジェクトは 3 つの属性を持ちます。

1. **アイデンティティ（identity）**：メモリ上の一意な ID。`id()` で取得でき、生存中は不変。
2. **型（type）**：そのオブジェクトが何であるか。`type()` で取得。
3. **値（value）**：中身。

ここで決定的に重要なのは、**変数は「値」を入れる箱ではなく、「オブジェクトに付けた名札（参照）」だ**ということです。`x = 1` は「`x` という箱に 1 を入れる」のではなく、「`1` というオブジェクトに `x` という名札を貼る」操作です。

```python
a = [1, 2, 3]
b = a            # b は「a と同じリスト」に別の名札を貼っただけ（コピーではない）

b.append(4)
print(a)         # → [1, 2, 3, 4]   ← a も変わる！ 同じオブジェクトだから

print(a is b)    # → True           ← 同一オブジェクト（同じ id）
print(id(a) == id(b))  # → True
```

この「代入はコピーではなく参照の共有」というモデルこそ、Python の可変オブジェクトにまつわるバグの**唯一にして最大の原因**です。逆に、ここを理解していれば、後述するすべての落とし穴は「当たり前の帰結」として見えてきます。

> 💡 **JavaScript / Java を書く人へ**：感覚は「プリミティブも含めてすべてが参照型」に近いと思ってください。ただし `int` や `str` は**不変（immutable）**なので、共有していても「中身を書き換えられて壊れる」ことがない——だから安全に見えるだけです。

---

## **1. 数値型：`int` / `float` / `Decimal` / `Fraction` / `complex`**

### **1-1. `int`：桁あふれしない任意精度整数**

多くの言語の整数は固定幅（32bit / 64bit）で、上限を超えると**オーバーフロー**します。Python の `int` は違います。**メモリが許す限りいくらでも大きくなる任意精度（arbitrary-precision）整数**です。

```python
2 ** 100        # → 1267650600228229401496703205376  桁あふれしない
factorial = 1
for i in range(1, 101):
    factorial *= i
len(str(factorial))   # → 158  （100! は158桁）
```

これは「C言語のような `int` オーバーフロー由来のセキュリティ脆弱性が、Python では原理的に起きない」という大きな利点です。代償は速度とメモリですが、通常のアプリ開発では問題になりません。

リテラルは基数を変えられ、桁区切りに `_` が使えます（Python 3.6+、PEP 515）。可読性のために積極的に使いましょう。

```python
0b1010        # 2進数 → 10
0o17          # 8進数 → 15
0xFF          # 16進数 → 255
1_000_000     # → 1000000   （アンダースコアは無視される。可読性のため）
```

`int` はビット演算（`&` `|` `^` `~` `<<` `>>`）やビット数取得（`(255).bit_length()` → 8、`(7).bit_count()` → 3）も備え、フラグ管理や低レベル処理にも耐えます。

### **1-2. `float`：速いが「正確ではない」**

`float` は **64bit の IEEE 754 倍精度浮動小数点数**です。CPU が直接扱えるので高速ですが、**10進数の小数を正確には表現できません**。これは Python のバグではなく、2進数で `0.1` を有限桁で表せないという数学的事実です。

```python
0.1 + 0.2          # → 0.30000000000000004   （0.3 ではない！）
0.1 + 0.2 == 0.3   # → False
```

初学者が必ず驚くこの挙動は、**金融・課金・在庫など「正確さが要件」の領域で、そのまま致命的なバグになります**。float を比較するときは厳密一致ではなく、許容誤差で比べます（Python 3.5+、PEP 485）。

```python
import math
math.isclose(0.1 + 0.2, 0.3)   # → True   （相対・絶対許容差で比較）
```

そしてもう一つの罠が `round()` です。Python の `round()` は学校で習う「四捨五入」ではなく、**偶数丸め（banker's rounding／round half to even）**です。統計的な偏りを抑える正しい挙動ですが、知らないと「なぜ切り上がらない？」と混乱します。

```python
round(0.5)    # → 0   （0.5 は偶数の 0 へ）
round(1.5)    # → 2
round(2.5)    # → 2   （2.5 は偶数の 2 へ。3 ではない）
```

特殊値も覚えておきます。`float('inf')`（無限大）、`float('nan')`（非数）があり、**`nan` は自分自身とすら等しくない**（`nan == nan` は `False`）という、条件分岐を壊しがちな性質を持ちます。`nan` の判定は必ず `math.isnan()` を使います。

### **1-3. 【最重要】お金を `float` で持ってはいけない — `Decimal` という正解**

ここが、本記事で最も実務に直結するセクションです。**金額・通貨・税率・課金額を `float` で扱った瞬間、あなたのシステムは「ずれる」ことが運命づけられます。**

```python
# アンチパターン：float で金額を足し込む
total = 0.0
for _ in range(10):
    total += 0.1
print(total)        # → 0.9999999999999999   （1.0 にならない）
```

1 円のずれが、10 万件の決済で積み上がれば、会計が合わなくなり、二重課金や返金漏れの温床になります。正解は `decimal.Decimal` です。**10進数を正確に保持し、丸めモードを明示的に制御できます。**

```python
from decimal import Decimal, ROUND_HALF_UP

# ① 必ず「文字列」から生成する（float から作ると誤差を引き継ぐ）
price = Decimal("19.99")
tax_rate = Decimal("0.10")

tax = (price * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
total = price + tax
print(total)        # → 20.99   （正確）

# ② float から作ると誤差が入る — 絶対に避ける
Decimal(0.1)        # → Decimal('0.1000000000000000055511151231257827021181583404541015625')
Decimal("0.1")      # → Decimal('0.1')   ← 文字列から作るのが鉄則
```

実務では、設計レベルでさらに二つの選択肢があります。

| 方式 | 内部表現 | 向くケース | 注意点 |
| --- | --- | --- | --- |
| `Decimal` | 10進固定小数 | 為替・税計算・複雑な丸め | DB との往復で型変換を厳密に |
| 整数の最小単位 | 円・銭・cent を整数で | 高頻度・高性能な決済処理 | 表示時に 100 で割る規約をチームで統一 |

たとえば「金額は常にセント（最小通貨単位）の `int` で持ち、表示の瞬間だけ通貨へ変換する」という設計は、Stripe をはじめ多くの決済システムが採用する定石です。筆者が決済信頼性レイヤーを主導したプラットフォームでは、**残高更新を原子的トランザクションと冪等性キーで守り、金額表現を `float` から完全に排除する**ことで、本番運用で二重課金0件を達成しました。「お金は `float` で持たない」は、お題目ではなく**現場の血で書かれたルール**です。

> 💡 **`Fraction` という選択肢**：有理数を「分子／分母」で厳密に保持したいなら `fractions.Fraction("1/3")` が使えます。`Fraction(1, 3) * 3 == 1`（誤差ゼロ）。確率・比率の累積計算で威力を発揮します。

### **1-4. `complex`：科学計算のための複素数**

Python は複素数を**言語組み込み**で持ちます。虚数は `j` で表します。信号処理・電気回路・フーリエ変換などで使います。

```python
z = 3 + 4j
z.real          # → 3.0
z.imag          # → 4.0
abs(z)          # → 5.0   （複素数の絶対値＝大きさ）
```

### **1-5. `bool`：実は `int` のサブクラス（隠れた罠）**

`True` / `False` は真偽値ですが、Python では **`bool` は `int` のサブクラス**で、`True == 1`、`False == 0` です。これは便利な反面、静かなバグを生みます。

```python
True + True         # → 2          （bool は int なので算術できる）
sum([True, False, True])  # → 2    （イテラブル中の True を数えるイディオム）

isinstance(True, int)     # → True  ← ここが落とし穴
```

最後の行が問題です。`isinstance(x, int)` で「整数だけ通したい」つもりでも、**`True` / `False` が通り抜けます**。境界で整数バリデーションを書くときは `type(x) is int` で厳密に判定するか、Pydantic のように `bool` を `int` から分離する検証器に任せます。

「真偽値らしさ（truthiness）」も重要な概念です。`if` の条件は `bool` でなくても評価され、**空のコレクション・`0`・`""`・`None` は偽**として扱われます。

```python
items = []
if not items:           # 空リストは偽 → Pythonic
    print("空です")

# アンチパターン: if len(items) == 0:  ← 冗長
# アンチパターン: if items == []:      ← 型に依存して脆い
```

> ⚠️ **truthiness の落とし穴**：「値が渡されたか」を `if value:` で判定すると、`0` や `""` や空リストといった**正当な値**まで「未指定」と誤判定します。「未指定」を区別したいときは、後述の `if value is None:` を使います。

---

## **2. 文字列 `str`：Unicode コードポイントの不変な並び**

### **2-1. str の本質：不変・Unicode・シーケンス**

`str` は **Unicode コードポイントの不変（immutable）なシーケンス**です。3 つのキーワードが全てを説明します。

- **不変**：一度作った文字列は変更できない。`s[0] = "X"` はエラー。「変更」は常に**新しい文字列の生成**。
- **Unicode**：`len("こんにちは")` は 5（バイト数ではなく文字数）。絵文字や結合文字には注意が必要だが、基本はコードポイント単位。
- **シーケンス**：インデックス・スライス・イテレーションができる。

```python
s = "Python"
s[0]            # → 'P'
s[-1]           # → 'n'
s[1:4]          # → 'yth'   （スライス。元を壊さず新しい str を返す）
s[::-1]         # → 'nohtyP' （逆順のイディオム）
len(s)          # → 6
```

文字列リテラルは多彩です。実務で押さえるべきは次の通り。

```python
name = "友田"
# f-string（3.6+）：最も推奨される文字列整形
greeting = f"こんにちは、{name}さん"
# f-string の = デバッグ（3.8+）：変数名と値を同時に出す
value = 42
print(f"{value=}")          # → value=42

raw = r"C:\Users\path"      # raw 文字列：\ をエスケープとして扱わない（正規表現・パスで必須）
multi = """複数行を
そのまま書ける"""             # 三連クォート
```

### **2-2. 「+= でループ連結」をしてはいけない理由**

文字列は不変なので、ループ内で `+=` すると**毎回新しい文字列を作り直し**、最悪 O(n²) になります。正解は `str.join()` です。

```python
# アンチパターン：O(n²) になりうる
result = ""
for word in words:
    result += word          # 毎回新オブジェクト生成

# 正解：O(n)。可読性も高い
result = "".join(words)
```

> 💡 CPython には `+=` を最適化する実装がありますが、**実装依存で保証されません**。`join` は仕様として速く、意図も明確です。「正しいイディオムを選ぶ」のは、性能だけでなく**可読性と移植性**の問題です。

主要メソッドは「検索・変換・分割・結合・判定」で整理して覚えると応用が効きます。`strip()` / `lower()` / `upper()` / `replace()` / `split()` / `startswith()` / `endswith()` / `find()` / `format()`。大文字小文字を無視した比較は、`lower()` ではなく `casefold()`（より厳密な Unicode 折り畳み）を使うのが正解です。

### **2-3. `str` と `bytes`：テキストとバイナリの境界**

ここはネットワーク・ファイル・暗号を扱うと必ずぶつかる壁です。

- **`str`**：人間が読むテキスト（Unicode コードポイント）。
- **`bytes`**：機械が扱う生のバイト列（0–255 の不変な並び）。`b"..."` で書く。

両者の変換は**明示的なエンコーディング**を介します。`str → bytes` は `encode()`、`bytes → str` は `decode()`。文字化けの 9 割は、ここで**エンコーディングを暗黙に任せた**ことが原因です。常に `utf-8` を明示しましょう。

```python
text = "日本語"
data = text.encode("utf-8")   # → b'\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e'（9バイト）
data.decode("utf-8")          # → '日本語'

len(text)                     # → 3   （文字数）
len(data)                     # → 9   （バイト数。UTF-8 で日本語は1文字3バイト）
```

可変なバイト列が必要なら `bytearray`、メモリコピーなしでバイト列を覗くなら `memoryview` を使います。大きなバイナリを扱うバックエンドでは、この使い分けがメモリ効率を左右します。

---

## **3. `None`：値が「ない」ことを表す唯一の存在**

`None` は「値がない」「未設定」「該当なし」を表す特別なオブジェクトで、型は `NoneType`、**プログラム全体でただ一つしか存在しません（シングルトン）**。だからこそ、`None` の比較は `==` ではなく **`is` を使う**のが鉄則です。

```python
result = None

if result is None:        # 正解：アイデンティティで比較
    ...

# アンチパターン：== は __eq__ をオーバーライドした型で誤動作しうる
# if result == None:
```

`None` を返す関数（DB に該当レコードがない、など）は頻出です。型ヒントでは `X | None`（Python 3.10+。それ以前は `Optional[X]`）で「`X` か、なければ `None`」を表現します。これは「呼び出し側に `None` チェックを強制する」**契約**であり、`NoneType has no attribute ...` という最頻出エラーを静的解析で潰す武器になります。

```python
def find_user(user_id: int) -> "User | None":
    ...

user = find_user(1)
user.name             # mypy / pyright が「None かもしれない」と警告 → 事故を防ぐ
if user is not None:
    user.name         # ここでは安全
```

> 💡 **センチネルパターン（上級）**：`None` 自体が正当な値になりうる API（例：「キーは存在するが値が `None`」）では、`None` を「未指定」の印に使えません。そのときは `_MISSING = object()` のような**専用のセンチネルオブジェクト**を作り、`if value is _MISSING:` で区別します。`dict.get(key, default)` の内部もこの発想です。

---

## **4. コレクション：可変性・順序・ハッシュ可能性・計算量で選ぶ**

ここが設計力の差が最も出る領域です。`list` / `tuple` / `dict` / `set` を「なんとなく」で選ぶのではなく、**4 つの軸**で判断します。

1. **可変か（mutable / immutable）**：作った後で中身を変えるか。
2. **順序を保つか**：並び順に意味があるか。
3. **ハッシュ可能か**：`dict` のキーや `set` の要素にできるか（＝不変であることが条件）。
4. **計算量（Big-O）**：その操作はスケールするか。

### **4-1. `list`：可変な動的配列**

順序があり、変更でき、重複を許す——最も汎用的なコレクション。内部は動的配列なので、**末尾追加は速い（償却 O(1)）が、先頭への挿入・削除は遅い（O(n)）**です。

```python
nums = [3, 1, 4, 1, 5]
nums.append(9)          # 末尾追加：償却 O(1)
nums.insert(0, 2)       # 先頭挿入：O(n)（全要素をずらす）
nums.sort()             # その場ソート：O(n log n)
9 in nums               # メンバーシップ判定：O(n) ← 大きいと遅い

squares = [x * x for x in range(5)]   # リスト内包表記：速くて読みやすい
```

> 💡 **先頭操作が多いなら `collections.deque`**：両端キュー。`appendleft` / `popleft` が O(1)。FIFO キューやスライディングウィンドウで `list.pop(0)` を使っているコードは、`deque` に替えるだけで劇的に速くなります。Real Python の入門記事は触れませんが、現場では頻出の最適化です。

### **4-2. `tuple`：不変で軽量、だから「キー」になれる**

`tuple` は不変な `list` のようなものですが、用途は明確に異なります。**「変わらない・変えてはいけないデータの組」**を表すために使います。不変ゆえに**ハッシュ可能**で、`dict` のキーや `set` の要素にできます。

```python
point = (35.6895, 139.6917)     # 緯度・経度：意味のある固定の組
# point[0] = 0  ← TypeError（不変なので安全）

# 複数戻り値は実はタプル
def divmod_(a, b):
    return a // b, a % b        # (商, 余り) というタプル
q, r = divmod_(17, 5)           # アンパック代入

cache = {(35.68, 139.69): "Tokyo"}   # タプルを dict のキーに（list ではできない）
```

「読み取り専用であることを型で表明したい」「座標やキーのように組で意味を持つ」場面では `list` ではなく `tuple` を選ぶ——これだけで、意図が明確になり、誤った変更がコンパイル/実行前に防げます。

### **4-3. `dict`：キーと値のマッピング（挿入順を保持）**

`dict` はキーから値への O(1) 平均のマッピング。**Python 3.7 以降、挿入順の保持が言語仕様として保証**されています（3.6 は CPython の実装詳細でした）。キーは**ハッシュ可能（＝不変型）**でなければなりません。

```python
user = {"id": 1, "name": "友田", "role": "engineer"}

user.get("email")               # キーがなければ None（KeyError を出さない）
user.get("email", "未設定")      # デフォルト付き取得
user.setdefault("tags", []).append("python")  # なければ作って操作

# 内包表記とマージ（3.9+ の | 演算子）
squared = {k: v * v for k, v in {"a": 2, "b": 3}.items()}
merged = {"a": 1} | {"b": 2}    # → {'a': 1, 'b': 2}
```

`KeyError` を避ける `.get()` / `.setdefault()`、集計の定番 `collections.Counter`、キー欠損時に自動生成する `collections.defaultdict` は、現場の `dict` 操作を一段クリーンにします。

```python
from collections import Counter, defaultdict

Counter("mississippi")          # → Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})

groups = defaultdict(list)
for word in ["apple", "avocado", "banana"]:
    groups[word[0]].append(word)   # 'a'/'b' キーを自動生成
```

### **4-4. `set` / `frozenset`：重複排除と高速メンバーシップ**

`set` は**順序を持たない・重複しない・要素はハッシュ可能**なコレクション。最大の価値は、**メンバーシップ判定が O(1)**であることと、**集合演算**が言語レベルで書けることです。

```python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a & b           # 積集合（共通）→ {3, 4}
a | b           # 和集合 → {1, 2, 3, 4, 5, 6}
a - b           # 差集合 → {1, 2}
a ^ b           # 対称差 → {1, 2, 5, 6}

# 重複排除のイディオム
unique = list(set([1, 1, 2, 2, 3]))   # → [1, 2, 3]（順序は保証されない点に注意）

3 in a          # メンバーシップ：O(1) ← list の O(n) と決定的に違う
```

**「大量データに `x in some_list` を繰り返す」コードは、`some_list` を `set` に変えるだけで O(n²) が O(n) になります。**これは現場で最も費用対効果の高い最適化の一つです。`frozenset` は不変版で、`dict` のキーや `set` の要素にできます。

### **4-5. 計算量の早見表（これが「使い分け」の核心）**

主要操作の平均計算量です。型選択は最終的にこの表に集約されます。

| 操作 | list | deque | dict | set |
| --- | --- | --- | --- | --- |
| 末尾の追加 | 償却 O(1) | O(1) | — | — |
| 先頭の追加 | O(n) | O(1) | — | — |
| インデックス参照 | O(1) | O(n) | — | — |
| キー / 要素の検索（in） | O(n) | O(n) | O(1) | O(1) |
| キーで値を取得 | — | — | O(1) | — |
| 要素の削除（任意） | O(n) | O(n) | O(1) | O(1) |

判断の指針はシンプルです。**「順序つきで反復するなら `list`、両端操作なら `deque`、キーで引くなら `dict`、存在判定と重複排除なら `set`、変えない組なら `tuple`」**。

---

## **5. 可変性が生む本番バグ Top 3（ここで差がつく）**

データ型の知識を「事故防止」に変換するセクションです。動的型 Python のバグの大半は、この 3 つに集約されます。

### **5-1. ミュータブルデフォルト引数（Python 最悪の罠）**

関数のデフォルト引数は、**関数定義時に一度だけ評価され、呼び出しをまたいで共有**されます。可変オブジェクトをデフォルトにすると、前回の呼び出しの結果が次回に漏れます。

```python
# アンチパターン：空リストをデフォルトに
def add_item(item, basket=[]):
    basket.append(item)
    return basket

add_item("apple")     # → ['apple']
add_item("banana")    # → ['apple', 'banana']  ← 前回の 'apple' が残る！
```

正解は **`None` センチネル**です。「呼び出しごとに新しいリストを作る」を明示します。

```python
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket
```

この罠は型ヒントでも防げず、レビューでしか止まりません。だからこそ、**「可変デフォルトは即 None センチネル」**を反射で書けるようにしておくのが、プロの最低条件です。

### **5-2. 共有参照と「浅いコピー」**

セクション 0 の「代入はコピーではない」が牙を剥くのがコピーです。`copy()` やスライスは**浅いコピー（shallow copy）**——一段目は複製しますが、**ネストした要素は共有したまま**です。

```python
import copy

original = [[1, 2], [3, 4]]
shallow = original[:]              # 浅いコピー
shallow[0].append(99)
print(original)                   # → [[1, 2, 99], [3, 4]]  ← 内側が共有されている！

deep = copy.deepcopy(original)    # 深いコピー：再帰的に複製。完全に独立
```

「設定 dict を複製して一部だけ変える」「テストのフィクスチャを使い回す」といった場面で、これは静かにデータを汚染します。**ネストがあるなら `deepcopy`、あるいはそもそも不変型（`tuple` / `frozenset`）で設計**するのが安全です。

### **5-3. ハッシュ不可能な型をキーにしようとする**

`dict` のキーや `set` の要素は**ハッシュ可能**でなければなりません。`list` / `dict` / `set` は可変なのでハッシュ不可、`int` / `str` / `tuple`（中身が全てハッシュ可能なら）はハッシュ可能です。

```python
{[1, 2]: "x"}              # TypeError: unhashable type: 'list'
{(1, 2): "x"}              # OK：tuple はハッシュ可能
{(1, [2]): "x"}            # TypeError：中に list を含む tuple は不可
```

この性質を逆手に取り、「**ハッシュ可能性で『不変であるべき値』を型に表明する**」のが上級者の設計です。座標やキーは `tuple` で、定数の集合は `frozenset` で持つ——型がそのまま「変えてはいけない」というドキュメントになります。

### **おまけ：`is` と `==`、そして小整数キャッシュ**

- **`==`** は**値**の比較（`__eq__` を呼ぶ）。
- **`is`** は**アイデンティティ**（同一オブジェクトか）の比較。

`is` は `None` / `True` / `False` / センチネルにのみ使い、**値の比較には絶対に使わない**こと。理由は CPython の「小整数キャッシュ」にあります。CPython は `-5`〜`256` の `int` を再利用するため、次のような**実装依存の罠**が生まれます。

```python
a = 256; b = 256
a is b          # → True   （キャッシュされた同一オブジェクト）

a = 257; b = 257
a is b          # → False  （別オブジェクト。環境により変わる）
257 == 257      # → True   ← 値の比較は常に正しい。これを使う
```

`a is b` が `True` になることに依存したコードは、数値が 256 を超えた瞬間に壊れます。**「等しいかは `==`、同一かは `is`」**を機械的に守りましょう。

---

## **6. 型の確認：`type()` ではなく `isinstance()`、そしてダックタイピング**

実行時に型を確認する方法は 2 つあります。

```python
type(42) is int            # 厳密に int か（サブクラスは弾く）
isinstance(42, int)        # int か、その「サブクラス」か
isinstance(x, (int, float))  # 複数候補のいずれか
```

原則として **`isinstance()` を使います**。継承や抽象基底クラスを尊重するため、より柔軟で正しいからです。ただしセクション 1-5 で見たように、「`bool` を弾いて `int` だけ通す」ような厳密判定が必要なときだけ `type(x) is int` を使います。

さらに Python らしいのは、**ダックタイピング**——「型が何かではなく、必要な振る舞いを持つか」で判断する考え方です。`collections.abc` の抽象基底クラスを使うと、具体型に縛られず「反復できるか」「マッピングか」を判定できます。

```python
from collections.abc import Iterable, Mapping

def total(values):
    if not isinstance(values, Iterable):   # list でも set でも generator でも OK
        raise TypeError("反復可能オブジェクトが必要です")
    return sum(values)
```

これは「具体型ではなく抽象（プロトコル）に依存する」という、拡張に強い設計（CLAUDE.md でいう ETC）そのものです。

---

## **7. 動的型に「静的な安全」を与える：型ヒント**

Python は動的型付けですが、**型ヒント（PEP 484）**で型を注釈でき、`mypy` / `pyright` といった静的解析器が**実行前にバグを検出**してくれます。型ヒントは実行時には強制されませんが、現代の本番 Python では**事実上必須**です。

```python
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

# 3.9+ では組み込み型がそのままジェネリックに（PEP 585）
def first(items: list[int]) -> int | None:
    return items[0] if items else None

from typing import Final, Literal
MAX_RETRIES: Final = 3                          # 再代入を静的に禁止
def set_mode(mode: Literal["r", "w", "a"]) -> None: ...   # 取りうる値を型で限定
```

筆者の現場では、**「`any` 相当（型を諦める）を禁止し、境界で型を確定させ、CI で型チェックを必須化する」**規律を徹底します。動的言語でも、これで「不正な状態を表現不能にする」設計に近づけます。同じ思想を TypeScript で突き詰めた実践は [TypeScript 型安全の規律（Zod・NeverError・no-any）](/blog/typescript-type-safety-discipline-zod-nevererror-no-any) にまとめています。言語は違えど、**「境界で検証し、内側は型で守る」**という原則は完全に共通です。

---

## **8. 標準の型を超えて「自分の型を設計する」**

世界最高峰のコードと普通のコードの差は、**「組み込み型をそのまま使う」か「ドメインに合った型を設計する」か**に表れます。`dict` で何でも表現するのをやめ、意図を型で語らせます。

### **8-1. `dataclass`：構造化データの第一選択**

`@dataclass`（3.7+）は、`__init__` / `__repr__` / `__eq__` を自動生成し、ボイラープレートを消します。**`frozen=True` で不変に、`slots=True`（3.10+）でメモリ効率と速度を改善**できます。

```python
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Money:
    amount: int          # 最小通貨単位（cent）で持つ
    currency: str = "JPY"

@dataclass
class Order:
    id: int
    items: list[str] = field(default_factory=list)   # ← 可変デフォルトの正しい書き方

m = Money(1999)          # 不変なのでハッシュ可能・安全に共有できる
```

`field(default_factory=list)` に注目してください。これはセクション 5-1 のミュータブルデフォルト問題を、**dataclass が正しく解決する公式の作法**です。

### **8-2. `Enum`：「文字列の定数」をやめる**

状態や種別を生の文字列で持つと、`"acitve"` のようなタイポが実行時まで露見しません。`Enum`（`StrEnum` は 3.11+）で**取りうる値を閉じた集合**にすれば、不正値を型で排除できます。

```python
from enum import StrEnum

class OrderStatus(StrEnum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"

status = OrderStatus.PAID
status == "paid"         # → True（StrEnum は str でもある）
# OrderStatus("unknown") → ValueError（不正値を即座に弾く）
```

### **8-3. `NamedTuple` / `TypedDict`：軽量な型付け**

- **`NamedTuple`**：不変・タプル互換で、フィールドに名前をつけたいとき。
- **`TypedDict`**：JSON のように「`dict` の形」を型で表明したいとき（API レスポンスの型付けに最適）。

```python
from typing import NamedTuple, TypedDict

class Point(NamedTuple):
    x: float
    y: float

class UserDict(TypedDict):
    id: int
    name: str
    email: str | None
```

「`dict` を渡し回す」コードを `dataclass` / `TypedDict` に置き換えるだけで、IDE 補完が効き、タイポが消え、リファクタが安全になります。これは**保守性（maintainability）への直接投資**です。

---

## **9. システム境界では型を「検証」する：Pydantic / marshmallow**

最後に、これまでの知識を**本番アーキテクチャ**に接続します。型ヒントは「内部のコードを守る盾」ですが、**実行時には強制されません**。だから、HTTP リクエスト・外部 API レスポンス・環境変数・メッセージキューといった**システム境界の外から来るデータは、必ず実行時に検証**しなければなりません。「外から来るデータを信頼しない」——これがセキュアなバックエンドの第一原則です。

この境界に立つのが **Pydantic v2** と **marshmallow** です。

```python
from pydantic import BaseModel, EmailStr, Field

class CreateUser(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    email: EmailStr
    age: int = Field(ge=0, le=150)

# 不正な dict（外部入力）を渡すと ValidationError で堰き止める
user = CreateUser.model_validate({"name": "友田", "email": "a@example.com", "age": 30})
```

ここまで読んだあなたは、もう理解しているはずです。**Pydantic / marshmallow は、本記事で学んだ「型」を実行時の契約に変える装置**です。`int` の範囲、`str` の長さ、`None` 許容（Optional）、ネストした構造——すべてを宣言的に検証し、**信頼できるデータだけを内側へ通す**。動的言語の柔軟さを保ちながら、静的言語並みの堅牢さを境界で獲得する、というのが現代 Python バックエンドの設計の到達点です。

- 型ファーストの境界検証 → [Pydantic v2 実践ガイド](/blog/pydantic-v2-production-validation-type-safety)
- ORM / フレームワーク非依存のシリアライズ／検証 → [marshmallow 実践ガイド](/blog/marshmallow-python-serialization-validation-production-guide)
- Web フレームワーク層での入力検証 → [FastAPI 本番運用ガイド](/blog/fastapi-production-async-pydantic-observability-guide) と [FastAPI のリクエスト検証](/blog/fastapi-request-validation-query-path-body-parameters-guide)
- 永続化層の型安全 → [SQLAlchemy 2.0 実践ガイド](/blog/sqlalchemy-2-typed-orm-production-guide)
- お金と冪等性の設計 → [決済の二重課金を防ぐ冪等性設計](/blog/payment-double-charge-prevention-idempotency-procurement-guide)

---

## **まとめ：データ型を「制約」として設計する**

Python のデータ型は、暗記すべき一覧ではなく、**「正しさ・速さ・安全を、コードの構造で表現するための語彙」**です。本記事の要点を、判断軸として持ち帰ってください。

1. **すべてはオブジェクト**。変数は参照。だから可変性・`is`/`==`・コピーの挙動はすべて一本の線でつながる。
2. **お金は `float` で持たない**。`Decimal`（文字列から生成）か整数の最小単位で。これは現場の鉄則。
3. **コレクションは計算量で選ぶ**。存在判定と重複排除は `list` ではなく `set` / `dict`（O(1)）。
4. **可変性のバグを反射で防ぐ**。ミュータブルデフォルトは `None` センチネル、ネストは `deepcopy` か不変型。
5. **型を設計する**。`dataclass` / `Enum` / `TypedDict` で不正な状態を表現不能にし、境界は Pydantic / marshmallow で検証する。

「動的型だから雑でいい」のではありません。**動的型だからこそ、型に対する深い理解と規律が、本番品質を分けます。** 筆者は、この原則を Python / Flask / SQLAlchemy のバックエンドや、本番二重課金0件を達成した決済基盤で実践してきました。データ型を「制約として設計する」発想こそが、テストをすり抜けるバグを未然に消し、変更に強いコードベースを作ります。

---

## **よくある質問（FAQ）**

### Q. Python のデータ型は結局いくつ覚えればいい？

実務で頻出するのは **数値（`int` / `float` / `Decimal`）・文字列（`str` / `bytes`）・真偽値（`bool`）・None・コレクション（`list` / `tuple` / `dict` / `set`）** の約 10 種です。まずこれらの「可変性・順序・ハッシュ可能性・計算量」を押さえれば、9 割の場面に対応できます。

### Q. `list` と `tuple` はどう使い分ける？

**変更するなら `list`、変更しない（してはいけない）組なら `tuple`** です。`tuple` は不変ゆえにハッシュ可能で、`dict` のキーや `set` の要素にできます。座標・複数戻り値・固定レコードは `tuple`、要素を追加・削除する集まりは `list` が自然です。

### Q. なぜ `0.1 + 0.2` が `0.3` にならないの？ バグ？

バグではありません。`float` は 2 進数の浮動小数点（IEEE 754）で、`0.1` を有限桁で正確に表現できないためです。比較は `math.isclose()`、金額計算は `Decimal` か整数の最小単位を使います。

### Q. `is` と `==` はどちらを使うべき？

**値が等しいかは `==`、同一オブジェクトかは `is`** です。`is` は `None` / `True` / `False` / センチネルの判定にのみ使い、数値や文字列の比較には使いません（CPython の小整数・文字列キャッシュに依存して壊れます）。

### Q. 型ヒントは実行時に効くの？ 書く意味はある？

実行時には強制されません（無視されます）が、**`mypy` / `pyright` が実行前にバグを検出**してくれるため、本番コードでは事実上必須です。実行時に外部入力を検証したいなら、Pydantic や marshmallow を併用します。
