# Python のマッピング完全ガイド：dict の内部・collections の使い分け・自作マッピング設計と本番運用

> Pythonのマッピング（キーと値の対応）を、dictの挙動と内部、collections（defaultdict / Counter / OrderedDict / ChainMap）と types.MappingProxyType、collections.abc / UserDict による自作マッピング、構造的パターンマッチ、__hash__/__eq__ の契約、境界での型検証まで体系化。dictを『使う』から『設計する』へ引き上げる、本番品質の実務ガイドです。

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

## 要点

- マッピングは『キー → 値の対応』という抽象で、dict はその一実装にすぎない。collections.abc.Mapping / MutableMapping を理解すると、dict を超えた設計ができる
- 標準ライブラリの defaultdict / Counter / OrderedDict / ChainMap / MappingProxyType を使い分けると、集計・設定の階層化・読み取り専用APIが宣言的に書ける
- 自作マッピングは dict を直接継承してはいけない（C実装が Python の override を迂回する）。collections.UserDict か MutableMapping を継承するのが正解
- 辞書のキーにする自作オブジェクトは __hash__ と __eq__ の契約を守る。@dataclass(frozen=True) が安全な定石で、eq だけ定義すると unhashable になる罠がある
- 外部入力の JSON は『ただの dict』にすぎない。境界では TypedDict ではなく Pydantic / marshmallow で実行時検証し、信頼できる型だけを内側へ通す

---

## **導入：dict を「使う」人と、マッピングを「設計する」人の差**

Python を書く人なら、誰もが `dict` を毎日使います。しかし、現場で評価が分かれるのは「`dict` を使えるか」ではなく、**「`dict` が体現している『マッピング』という抽象を理解し、必要に応じて自分のマッピング型を設計できるか」**です。

なぜ集計コードに `defaultdict` や `Counter` を使うと一気に読みやすくなるのか。なぜ設定の優先順位は `ChainMap` で「コピーせずに」重ねられるのか。なぜ公開 API で内部の `dict` をそのまま返すと事故になり、`MappingProxyType` が必要なのか。そして——**なぜ `dict` を継承してメソッドを上書きすると、半分しか効かないのか**。これらはすべて、「マッピングというプロトコル（抽象）」を理解していれば設計判断として説明できることです。

本記事は、世界中で読まれている [Real Python の "Python Mappings"](https://realpython.com/python-mappings/) が扱う範囲——マッピングの定義、`collections.abc` の抽象基底クラス、標準ライブラリのマッピング群、自作マッピング——を**土台にしつつ**、そこから先の「本番で効く設計知」まで踏み込みます。具体的には、

- マッピング・プロトコル（`Mapping` / `MutableMapping`）の**正確な契約**と、継承で「タダで手に入るメソッド」
- `defaultdict` / `Counter` / `OrderedDict` / `ChainMap` / `MappingProxyType` の**使い分けと落とし穴**
- **`dict` を直接継承してはいけない理由**と、`UserDict` / `MutableMapping` による正しい自作
- 自作オブジェクトを**キーにするための `__hash__` / `__eq__` 契約**
- 構造的パターンマッチ、本番の落とし穴、そして**システム境界での型検証**

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・実装し、`Router → UseCase → Repository` の厳格な層分離で本番運用してきました。「外部から来た `dict` を信頼しない」「内部状態を読み取り専用で公開する」といった本記事の原則は、すべてその実戦から得たものです。`dict` の基礎（ハッシュ可能性・挿入順保持・計算量）は前作の [Python のデータ型 完全ガイド](/blog/python-data-types-complete-guide) で扱ったので、本記事は**その一段上、「マッピングを設計する」**ところに集中します。

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

---

## **1. マッピングとは何か：`dict` は「実装の一つ」にすぎない**

**マッピング（mapping）とは、「キーから値への対応」を表すコレクション**です。Python の世界では `dict` が最も有名で高速な実装ですが、`dict` だけがマッピングではありません。`defaultdict`、`Counter`、`ChainMap`、`OrderedDict`、`MappingProxyType`、そして**あなたが自作するクラス**も、すべて「マッピング」です。

ここで重要なのは、**「マッピングである」とは「特定の振る舞い（プロトコル）を満たす」こと**であって、「`dict` を継承していること」ではない、という点です。これは「具体的な実装ではなく抽象に依存する」という設計原則（ETC: Easy To Change）の、Python における具体例です。

この「振る舞いの契約」を形にしたのが、`collections.abc` の抽象基底クラス（ABC）です。

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

isinstance({}, Mapping)              # → True
isinstance({}, MutableMapping)       # → True

from types import MappingProxyType
isinstance(MappingProxyType({}), Mapping)         # → True
isinstance(MappingProxyType({}), MutableMapping)  # → False  ← 読み取り専用だから
```

最後の例が、抽象の威力です。`MappingProxyType`（読み取り専用）は `Mapping` だが `MutableMapping` ではない。だから関数の引数で `Mapping` を要求すれば「読むだけ」を、`MutableMapping` を要求すれば「書き換える」ことを、**型で表明**できます。

```python
def render(config: Mapping[str, str]) -> str:
    # config を変更しないことを「型」で約束している（読み取り専用契約）
    return "\n".join(f"{k}={v}" for k, v in config.items())
```

---

## **2. マッピング・プロトコルの「契約」：何を実装すれば何がタダで手に入るか**

ここが Real Python の記事の核心であり、自作マッピングを理解する鍵です。`collections.abc` の ABC を継承すると、**少数の「抽象メソッド」を実装するだけで、大量の「ミックスインメソッド」が自動的に手に入ります**。

| 継承する ABC | 自分で実装する抽象メソッド | タダで手に入るミックスイン |
| --- | --- | --- |
| Mapping（読み取り専用） | `__getitem__` / `__iter__` / `__len__` | `__contains__` / `keys` / `items` / `values` / `get` / `__eq__` / `__ne__` |
| MutableMapping（読み書き） | 上記 ＋ `__setitem__` / `__delitem__` | 上記 ＋ `pop` / `popitem` / `clear` / `update` / `setdefault` |

つまり、**`MutableMapping` を継承すれば、わずか 5 メソッド（`__getitem__` / `__setitem__` / `__delitem__` / `__iter__` / `__len__`）を実装するだけで、`dict` とほぼ同じ API を持つ完全なマッピングが完成**します。しかも、`get` も `update` も `__contains__` も、あなたが実装した 5 メソッドを**経由して**動くため、振る舞いが一貫します。これがプロトコル設計の美しさです。

> 💡 **なぜこれが「世界最高峰」の設計につながるのか**：ミックスインは DRY（Don't Repeat Yourself）の極致です。`get` や `update` のロジックを自分で書けば、必ずどこかでバグります。ABC を継承すれば、それらは**一度だけ正しく書かれた標準実装**を使い回せる。あなたは「このマッピング固有の本質（5 メソッド）」だけに集中できます。これは SRP（単一責任）の実践でもあります。

---

## **3. 標準ライブラリの強力なマッピング群（ここで生産性が変わる）**

`dict` で何でも書けますが、**目的に合ったマッピングを選ぶと、コードが宣言的になり、意図が一目で伝わります**。これは KISS（単純さ）の実践です。

### **3-1. `defaultdict`：キー欠損を「初期値の自動生成」で吸収する**

集計・グルーピングの定番。存在しないキーにアクセスすると、`default_factory` が初期値を作って挿入します。

```python
from collections import defaultdict

groups = defaultdict(list)
for name in ["apple", "avocado", "banana"]:
    groups[name[0]].append(name)   # キーがなければ [] を自動生成して append
# → defaultdict(<class 'list'>, {'a': ['apple', 'avocado'], 'b': ['banana']})
```

`dict` なら `groups.setdefault(name[0], []).append(name)` と書くところを、宣言的に置き換えられます。ただし**落とし穴**があります。

```python
counts = defaultdict(int)
_ = counts["nonexistent"]      # ← 読んだだけのつもりが、キーが作られる！
print(dict(counts))            # → {'nonexistent': 0}
```

「存在確認のつもりでアクセスしたら、副作用でキーが増殖していた」というバグは頻出です。**読み取り専用で触りたいときは `defaultdict` でも `.get()` を使う**こと。`d[key]` と `d.get(key)` は意味が違います。

### **3-2. `Counter`：多重集合（マルチセット）としての辞書**

要素の出現回数を数える専用マッピング。`most_common()` や算術演算まで備えます。

```python
from collections import Counter

votes = Counter(["a", "b", "a", "c", "a", "b"])
votes.most_common(2)        # → [('a', 3), ('b', 2)]   頻度順
votes["zzz"]                # → 0   欠損キーは 0（KeyError を出さず、挿入もしない）

Counter("aab") + Counter("abc")   # → Counter({'a': 3, 'b': 2, 'c': 1})  加算
Counter("aab") & Counter("abc")   # → Counter({'a': 1, 'b': 1})  最小（積）
```

`Counter` の `__missing__` は 0 を返すため、`votes["zzz"]` は `defaultdict` と違って**キーを増やしません**。ランキング・出現頻度・在庫差分などで、手書きのカウントループを一掃できます。

### **3-3. `OrderedDict`：`dict` が順序を保つ今でも残る使い道**

「`dict` は 3.7 から挿入順を保つのに、`OrderedDict` はもう要らないのでは？」——半分正解です。普通の用途では `dict` で十分。しかし `OrderedDict` だけが持つ機能があります。

```python
from collections import OrderedDict

od = OrderedDict.fromkeys("abcd")
od.move_to_end("a")          # 'a' を末尾へ（dict にはない）
od.popitem(last=False)       # 先頭を取り出す → FIFO キューになる（dict は末尾固定）

# 等価性が「順序を区別する」
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1)   # → False  順序まで比較
dict(a=1, b=2) == dict(b=2, a=1)                 # → True   順序は無視
```

`move_to_end` と `popitem(last=False)` は **LRU キャッシュ**の構築に最適です（`functools.lru_cache` の内部発想）。「順序そのものが意味を持つ」場面では、いまも `OrderedDict` が正解です。

### **3-4. `ChainMap`：コピーせずに辞書を「重ねる」**

複数のマッピングを 1 枚に見せ、**先頭から順に検索**します。設定の優先順位（CLI > 環境変数 > デフォルト）を、辞書をマージ（コピー）せずに表現できる——これが効きます。

```python
from collections import ChainMap

defaults = {"theme": "light", "timeout": 30}
env      = {"timeout": 60}
cli      = {"theme": "dark"}

config = ChainMap(cli, env, defaults)   # 先頭ほど優先
config["theme"]     # → 'dark'   （cli が勝つ）
config["timeout"]   # → 60       （env が勝つ）
config["timeout"]   # defaults の 30 は env に隠される

# 書き込み・削除は「先頭のマップだけ」に作用する
config["theme"] = "system"
cli                 # → {'theme': 'system'}   ← defaults は不変のまま
```

辞書を `{**defaults, **env, **cli}` でマージするとメモリコピーが発生し、元の層を**後から差し替えられません**。`ChainMap` は層を保ったまま動的にオーバーレイできるため、設定管理やスコープ（変数の入れ子）に向きます。本格的な設定・シークレット管理は [Pydantic Settings による設定管理](/blog/pydantic-settings-configuration-management-secrets-guide) と組み合わせると堅牢です。

### **3-5. `MappingProxyType`：読み取り専用ビューで内部状態を安全に公開する**

ここは**セキュリティと API 設計**に直結する、現場で差がつく知識です。クラスやモジュールの内部 `dict` をそのまま外へ返すと、**呼び出し側に書き換えられて内部状態が壊れます**。`types.MappingProxyType` は、元の `dict` への**読み取り専用ビュー**を提供します。

```python
from types import MappingProxyType

_internal = {"version": "1.0", "debug": False}
PUBLIC = MappingProxyType(_internal)     # 読み取り専用の「窓」

PUBLIC["version"]      # → '1.0'
PUBLIC["x"] = 1        # → TypeError: 'mappingproxy' object does not support item assignment

_internal["debug"] = True
PUBLIC["debug"]        # → True   ← コピーではなく「ビュー」。元の変更は反映される
```

「内部は可変、公開は不変」を 1 行で実現できます。`dict(_internal)` でコピーを返す方法もありますが、それは**スナップショット**であって、毎回コピーコストがかかり、元の更新も追えません。`MappingProxyType` はコピーなし・常に最新・書き換え不能——**カプセル化の理想形**です。実際、Python のクラスの `__dict__` 属性もこの `mappingproxy` 型で公開されています。

---

## **4. 自作マッピングの設計：`dict` を継承してはいけない**

要件が標準型に収まらないとき（大文字小文字を無視する、書き込み時に検証する、キーを正規化する…）、自分のマッピング型を作ります。ここで **9 割の人がやる間違い**が、「`dict` を継承して `__getitem__` を上書きする」ことです。

### **4-1. なぜ `dict` の直接継承は壊れるのか**

```python
# アンチパターン：dict を継承して __getitem__ を上書きしても…
class UpperDict(dict):
    def __getitem__(self, key):
        return super().__getitem__(key.upper())

d = UpperDict()
d["ABC"] = 1
d["abc"]            # → 1      （__getitem__ は確かに効く）
d.get("abc")        # → None   ← get() は C 実装で、あなたの __getitem__ を呼ばない！
"abc" in d          # → False  ← __contains__ も迂回される
```

`dict` のメソッド（`get` / `update` / `__contains__` / `**` 展開など）は **C で実装**されており、内部で**あなたの Python の `__getitem__` を経由しません**。結果、「`d["abc"]` は効くのに `d.get("abc")` は効かない」という、デバッグ困難な**半壊状態**が生まれます。これは Python の有名な落とし穴です。

### **4-2. 正解その①：`collections.abc.MutableMapping` を継承する**

5 つの抽象メソッドを実装し、`get` / `update` / `__contains__` などはすべて ABC のミックスインに任せます。ミックスインはあなたの 5 メソッドを経由するので、**振る舞いが完全に一貫**します。

実例として、HTTP ヘッダのように**大文字小文字を区別しないマッピング**を作ります（`requests` ライブラリの `CaseInsensitiveDict` と同じ発想）。

```python
from collections.abc import MutableMapping
from typing import Iterator


class CaseInsensitiveDict(MutableMapping):
    """大文字小文字を区別しないマッピング。元のキー表記は保持する。"""

    def __init__(self, data: dict | None = None) -> None:
        # 小文字キー → (元のキー表記, 値) を保持する内部 dict
        self._store: dict[str, tuple[str, object]] = {}
        if data:
            self.update(data)   # MutableMapping.update が __setitem__ を呼ぶ

    def __setitem__(self, key: str, value: object) -> None:
        self._store[key.lower()] = (key, value)

    def __getitem__(self, key: str) -> object:
        return self._store[key.lower()][1]

    def __delitem__(self, key: str) -> None:
        del self._store[key.lower()]

    def __iter__(self) -> Iterator[str]:
        return (original for original, _ in self._store.values())

    def __len__(self) -> int:
        return len(self._store)


headers = CaseInsensitiveDict({"Content-Type": "application/json"})
headers["content-type"]       # → 'application/json'   __getitem__
headers.get("CONTENT-TYPE")   # → 'application/json'   ミックスインが __getitem__ 経由！
"content-Type" in headers     # → True                 __contains__ も一貫
list(headers)                 # → ['Content-Type']     元の表記を保持
```

`get` も `in` も、すべて期待どおり大文字小文字を無視します。`dict` 継承の「半壊」とは対照的に、**抽象メソッドを実装するだけで全 API が整合する**——これが ABC を使う最大の理由です。

### **4-3. 正解その②：`collections.UserDict`（dict 寄りの簡便版）**

「ほぼ `dict` のままで、一部だけ振る舞いを変えたい」なら `UserDict` が手軽です。内部に本物の `dict`（`self.data`）を持ち、メソッドが Python レベルで定義されているため、上書きが正しく合成されます。

```python
from collections import UserDict


class LoggingDict(UserDict):
    """書き込みを記録する観測可能なマッピング（可観測性の最小例）。"""

    def __setitem__(self, key, value):
        print(f"[audit] set {key!r}")     # 実務では structlog 等で構造化ログに
        super().__setitem__(key, value)
```

> ⚠️ **`UserDict` の注意点**：`UserDict` は `__contains__` を `self.data` に対して直接定義しています。そのため、`CaseInsensitiveDict` のように**キーの意味そのものを変える**場合は、`__getitem__` だけでなく `__contains__` も上書きが必要です。キー変換を伴う複雑な自作は `MutableMapping`、軽い味付けは `UserDict`——と使い分けます。

### **4-4. 読み取り専用の自作マッピング**

不変マッピングが欲しいだけなら、多くの場合 `MappingProxyType`（3-5）で十分です。値を**遅延計算**したい（アクセス時に動的に算出する）など、ロジックが必要なときだけ `collections.abc.Mapping` を継承し、`__getitem__` / `__iter__` / `__len__` の 3 つを実装します。`__setitem__` を実装しないので、**構造的に書き換え不能**になります。

---

## **5. マッピングをデータモデルに織り込む応用**

### **5-1. 自作オブジェクトをキーにする：`__hash__` / `__eq__` の契約**

マッピングのキーは**ハッシュ可能**でなければなりません。自作クラスをキーにするには、`__hash__` と `__eq__` を**一貫して**定義する必要があります。鉄則は「**等しいオブジェクトは等しいハッシュ値を持つ**」こと。これを破ると、辞書から値を取り出せなくなります。

最も安全な定石は **`@dataclass(frozen=True)`** です。`__eq__` と `__hash__` を矛盾なく自動生成してくれます。

```python
from dataclasses import dataclass

@dataclass(frozen=True)        # frozen=True で __hash__ が自動生成される
class GeoPoint:
    lat: float
    lng: float

cities = {GeoPoint(35.68, 139.69): "Tokyo"}
cities[GeoPoint(35.68, 139.69)]    # → 'Tokyo'   値が等しければ同じキー


# 罠：eq だけ定義し frozen にしないと、__hash__ が None にされる
@dataclass                     # eq=True（既定）, frozen=False（既定）
class Mutable:
    x: int

{Mutable(1): "v"}              # → TypeError: unhashable type: 'Mutable'
```

「`__eq__` を定義すると `__hash__` が無効化される」——これは「中身が変わりうるオブジェクトをキーにすると、ハッシュ位置がずれて壊れる」という Python の**安全装置**です。キーにするなら不変（frozen）にする、という設計が正しい理由がここにあります。

### **5-2. 構造的パターンマッチでマッピングを分解する（3.10+）**

`match` 文はマッピングに対しても使え、**「特定のキーを持つか」で分岐し、値を取り出す**ことができます。JSON イベントやコマンドのディスパッチを、ネストした `if` より遥かに読みやすく書けます。

```python
def handle(event: dict) -> str:
    match event:
        case {"type": "click", "x": int(x), "y": int(y)}:
            return f"click at ({x}, {y})"
        case {"type": "key", "code": str(code)}:
            return f"key {code}"
        case {"type": str(kind), **rest}:        # 残りのキーを rest で受ける
            return f"unhandled {kind} with {rest}"
        case _:
            return "not an event"

handle({"type": "click", "x": 10, "y": 20})   # → 'click at (10, 20)'
```

マッピングパターンは**部分一致**です（余分なキーがあってもマッチする）。`int(x)` のように**型でガード**しつつ束縛できるため、外部入力の安全な分解にも使えます。

### **5-3. 「stringly-typed な dict」をやめる判断**

`dict` は強力ですが、**何でも `dict[str, Any]` で持ち回るのは技術的負債**です。`user["emial"]`（タイポ）は実行時まで露見せず、IDE 補完も型チェックも効きません。「形が決まっているデータ」は、`dict` から**型のあるモデル**へ昇格させます。

```python
# アンチパターン：意味のある構造を生 dict で持つ
def total_price(order: dict) -> float:
    return order["price"] * order["quantity"]   # キー名はタイポし放題、型も不明

# 改善：dataclass で「形」を型にする
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Order:
    price: int          # 最小通貨単位（cent）で持つ
    quantity: int

def total_price(order: Order) -> int:
    return order.price * order.quantity         # 補完が効き、タイポは静的に落ちる
```

同じ思想を TypeScript で突き詰めた規律は [TypeScript 型安全の規律（Zod・NeverError・no-any）](/blog/typescript-type-safety-discipline-zod-nevererror-no-any) にまとめています。言語は違えど「データに形（型）を与え、不正な状態を表現不能にする」原則は共通です。

---

## **6. 本番の落とし穴：マッピングで実際に起きる事故**

### **6-1. 反復中の変更で `RuntimeError`**

`dict` を反復しながらサイズを変えると、実行時に落ちます。

```python
d = {"a": 1, "b": 2, "c": 3}
for k in d:
    if d[k] == 2:
        del d[k]          # → RuntimeError: dictionary changed size during iteration

# 正解：反復対象のスナップショットを固定する
for k in list(d):         # キーのリストを先に作る
    if d[k] == 2:
        del d[k]
```

### **6-2. 可変な dict を「共有」してしまう**

関数のデフォルト引数に `dict` を置く、クラス属性に `dict` を置く——いずれも**インスタンス間で共有**され、片方の変更が全体に漏れます。デフォルトは `None` センチネルで受けて関数内で生成し、クラス属性は `field(default_factory=dict)` を使います（詳細は前作 [Python のデータ型 完全ガイド](/blog/python-data-types-complete-guide) のミュータブルデフォルト引数を参照）。

### **6-3. スレッド安全性を GIL に頼らない**

「CPython の GIL があるから `dict` 操作はスレッドセーフ」という俗説は危険です。`d[k] = v` のような**単一操作**は原子的でも、「**確認してから代入**」のような複合操作は原子的ではありません。

```python
# 非原子的：チェックと代入の間に別スレッドが割り込みうる
if key not in counters:
    counters[key] = 0
counters[key] += 1          # 読み出し → 加算 → 書き戻しも非原子的（更新が消える）
```

しかも、フリースレッド版 CPython（PEP 703、3.13 で実験導入）では GIL 由来の暗黙の保護すら消えます。**共有マッピングへの複合操作は、明示的な `threading.Lock` で保護する**——これが移植性のある正解です。並行処理の信頼性設計は [リトライ・バックオフ・サーキットブレーカー](/blog/retry-backoff-circuit-breaker-resilience-patterns-guide) などの回復性パターンと併せて設計します。

---

## **7. 最重要：外部から来る dict を信頼しない（境界での型検証）**

ここまでの知識を**本番アーキテクチャ**に接続します。Web API のリクエストボディ、外部 API のレスポンス、設定ファイル——これらを `json.loads()` でパースした結果は、**型の保証が一切ない、ただの `dict[str, Any]`** です。これを検証せずに内側へ通すと、`KeyError` や `TypeError`、最悪の場合は不正なデータによるセキュリティ事故になります。

```python
import json

raw = json.loads('{"id": 1, "name": "友田"}')   # ← 型は dict[str, Any]。中身は無保証
```

`TypedDict` は「`dict` の形」を**静的な型注釈**として表現できますが、**実行時には検証しません**（注釈にすぎない）。実行時に「本当にこの形か」を保証するには、**Pydantic / marshmallow** を使います。

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

class CreateUser(BaseModel):
    id: int
    name: str = Field(min_length=1, max_length=50)
    email: EmailStr | None = None

# 外部の dict を検証して、信頼できる型に変える。不正なら ValidationError で堰き止める
user = CreateUser.model_validate(raw)
```

**マッピング（`dict`）はシステム境界の「共通通貨」**です。だからこそ、境界では必ず検証して、内側へは「検証済みの型」だけを通す——これがセキュアで壊れないバックエンドの第一原則です。

- 型ファーストの境界検証 → [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)
- LLM の JSON 出力をスキーマ検証する → [LLM 構造化出力の検証](/blog/pydantic-llm-structured-output-json-schema-validation-guide)

---

## **まとめ：マッピングを「設計の道具」として持つ**

Python のマッピングは、`dict` という一つの型ではなく、**「キー → 値の対応」という抽象と、その豊富な実装群、そして自作する能力**の総体です。要点を判断軸として持ち帰ってください。

1. **マッピングはプロトコル**。`Mapping`（読み取り専用）/ `MutableMapping`（読み書き）を型で要求すれば、契約が明確になる。
2. **目的に合った実装を選ぶ**。集計は `Counter`、グルーピングは `defaultdict`、設定の階層化は `ChainMap`、安全な公開は `MappingProxyType`。
3. **`dict` を直接継承しない**。自作は `MutableMapping`（キー意味を変えるなら）か `UserDict`（軽い味付け）。
4. **キーにする型は `__hash__`/`__eq__` の契約を守る**。`@dataclass(frozen=True)` が安全な定石。
5. **境界の dict は信頼しない**。`TypedDict` は静的注釈にすぎず、実行時検証は Pydantic / marshmallow で行う。

`dict` を「使う」だけなら誰でもできます。**マッピングを「設計する」と、コードは宣言的になり、不正な状態が表現不能になり、変更に強くなります。** 筆者は、この発想を経済産業大臣賞を受賞した B2B SaaS のバックエンドで実践し、`Router → UseCase → Repository` の各層でデータの形を型として設計してきました。「データに形を与える」ことこそ、テストをすり抜けるバグを未然に消し、保守性と拡張性を最大化する近道です。

---

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

### Q. `dict` と「マッピング」は何が違うの？

`dict` は「マッピング（キー → 値の対応）」という抽象の、最も高速で一般的な**実装の一つ**です。`defaultdict` / `Counter` / `ChainMap` / `MappingProxyType` や自作クラスもマッピングです。関数の引数で `collections.abc.Mapping` を要求すれば、`dict` に限定せず「マッピングらしいもの」全般を受け取れます。

### Q. `defaultdict` と `dict.setdefault()` はどちらを使うべき？

繰り返しグルーピング・集計するなら `defaultdict` が宣言的で読みやすいです。単発の「なければ初期化」なら `dict.setdefault()` で十分。ただし `defaultdict` は**アクセスしただけでキーを作る**ため、読み取り目的のときは `.get()` を使い分けてください。

### Q. `dict` が順序を保つ今、`OrderedDict` は不要では？

普通の用途では `dict` で十分です。ただし `move_to_end()` / `popitem(last=False)`（FIFO）や、**順序を区別する等価比較**が必要なら、いまも `OrderedDict` が正解です。LRU キャッシュの実装などで重宝します。

### Q. 自作の辞書を作りたい。`dict` を継承していい？

避けてください。`dict` のメソッド（`get` / `update` / `in` など）は C 実装で、あなたが上書きした `__getitem__` を呼ばないため、振る舞いが半分しか変わらず壊れます。`collections.UserDict`（軽い変更）か `collections.abc.MutableMapping`（キーの意味ごと変える）を継承するのが正解です。

### Q. JSON をパースした dict は、そのまま使っていい？

いいえ。`json.loads()` の結果は型保証のない `dict[str, Any]` です。境界では Pydantic / marshmallow で実行時検証し、`id` は整数か、必須キーはあるか、を確認してから内側へ通します。`TypedDict` は静的な型注釈にすぎず、実行時には検証しない点に注意してください。
