frozendict は MappingProxyType と何が違うのか
- Python 3.15 で追加された
frozendictって、MappingProxyTypeと何が違うの? - dict の代わりに使える? 型ヒントや lru_cache ではどう書くの?
- いざ使おうとしたら動かないケース(eval、copy など)を先に知りたい
対象読者: Python 3.14 までの知識で、3.15 の新機能を追っている方
前提知識: dict の基本操作と collections.abc.Mapping の存在が分かれば読めます
結論
作ったあとに変更できない辞書が、ビルトインの frozendict として使えます。キーと値が全部ハッシュ可能なら、インスタンス自体もハッシュ化されて dict のキーや lru_cache の引数に置けますよ。
cfg = frozendict(host="db.local", port=5432)
cfg["port"] = 5433
# TypeError: 'frozendict' object does not support item assignment
merged = cfg | frozendict(user="app")
print(merged)
# frozendict({'host': 'db.local', 'port': 5432, 'user': 'app'})
dict と何が違うのか
一番大きな違いは継承関係です。frozendict は dict のサブクラスではなく、object の直接の子孫です。実機で確認した結果がこちらです。
import json, pickle, copy
from collections.abc import Mapping
a = frozendict(x=1, y=2)
print("repr:", repr(a))
try:
a["z"] = 3
except TypeError as e:
print("assign:", e)
b = frozendict(y=2, x=1)
print("hash eq:", hash(a) == hash(b), "| == :", a == b)
print("is dict subclass:", issubclass(frozendict, dict))
print("isinstance dict:", isinstance(a, dict), "| Mapping:", isinstance(a, Mapping))
print("json:", json.dumps(a))
print("pickle roundtrip:", pickle.loads(pickle.dumps(a)))
print("copy.copy type:", type(copy.copy(a)).__name__)
print("dict(a):", dict(a))

つまり isinstance(f, dict) は False になります。既存コードで受け取り側を isinstance(arg, dict) と書いている箇所は、collections.abc.Mapping に置き換えるか (dict, frozendict) のタプルにします。等値比較は dict と互換性があって frozendict(x=1) == {"x": 1} は True、順序も等値比較には影響しません。
ハッシュ化と lru_cache
値が全部ハッシュ可能なら hash(frozendict) が動いて、ハッシュ値は項目の順序に依存しません。これが一番うれしい点で、PEP 814 の Rationale にも「dict を lru_cache に渡すとエラーになる」ことが動機として書かれています。
import functools
u = frozendict(x=1) | frozendict(y=2)
print("union:", u)
d = frozendict(x=1)
d |= frozendict(y=2)
print(d)
@functools.lru_cache
def f(cfg):
return sum(cfg.values())
print("lru_cache fd:", f(frozendict(a=1, b=2)))
try:
f({"a": 1, "b": 2})
except TypeError as e:
print("lru_cache dict:", e)
# 実行結果(Python 3.15.1)
# union: frozendict({'x': 1, 'y': 2})
# frozendict({'x': 1, 'y': 2})
# lru_cache fd: 3
# lru_cache dict: unhashable type: 'dict'
| と |= も使えます。ただし |= は in-place 更新ではなく新しい frozendict を作るので、元のオブジェクトは変わりません。また CPython では copy() が自分自身を返す実装になっています(PEP 814 の仕様どおり)。

つまずきポイント
実測で1つ引っかかった点があります。eval() や exec() の globals に frozendict を渡すと、Python が __builtins__ を注入しようとして TypeError になります。What’s New には「eval は frozendict の globals を受け付ける」と書いてあるのですが、builtins の注入ができないので実質そのままでは使い物になりません。
import builtins
try:
eval("x + y", frozendict(x=1, y=2))
except TypeError as e:
print(e)
g = frozendict(x=1, y=2, __builtins__=builtins)
print("eval with __builtins__:", eval("x + y", g))
# 実行結果(Python 3.15.1)
# cannot assign __builtins__ to frozendict globals
# eval with __builtins__: 3
回避するには、自分で __builtins__ をキーに持たせてから渡す必要があります。設定を渡して式を評価する用途なら、これで動きますね。

もう1点は浅いコピーです。値にリストのような可変オブジェクトを入れた場合、不変なのはあくまで「辞書の構造」だけで、中身のオブジェクトまでは守りません。設定オブジェクトとして共有するなら、値もタプルなど不変の型に揃えておくのが安全です。
検証環境
- Python 3.15.1(python.org 公式の Windows 64ビット embeddable 配布を一時ディレクトリに展開して実行)
- OS: Windows 11
- 実行したスクリプトとログは、各セクションの実行ログ画像にそのまま記載しています
【広告】XServerドメインならドメイン取得も運用もお得。![]()
まとめ
frozendict は「dict と同じ操作感で、あとから変えられないし、状況によってはハッシュ化できる」型です。設定や定数の受け渡しでうっかり書き換えを防ぎたい場面で、MappingProxyType より素直に使えます。
- dict のサブクラスではないので
isinstance(arg, dict)を使っている既存コードには要注意 - キーと値が全部ハッシュ可能なら dict のキーや lru_cache の引数にできる
- eval / exec の globals に渡すときは
__builtins__を自分で入れる
参考
- What’s new in Python 3.15 — PEP 814: Add frozendict built-in type
- PEP 814 – Add frozendict built-in type
- Built-in Types — frozendict(Python 3.15.1 ドキュメント)
- python/cpython#141510 — PEP 814: Add built-in frozendict type
あわせて読みたい
- 【Python】Python 3.15 移行ログ — 3.14 から上げて壊れた箇所と直し方
- 【Python】t-string 深掘り — PEP 750 の制約と f-string との違いを実測
- 【Python】flake8 + black + isort から Ruff 0.17 へ移行したログ — 実際に壊れた4箇所
- 【Python】Python 3.14 完全ガイド2026 — Free-Threading と JIT
【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!![]()

コメント