namedtuple(名前付きタプル)は、標準ライブラリ collections が提供する「フィールド名でアクセスできるタプル」です。p[0] の代わりに p.x と書けて、タプルなのでアンパックもインデックスもそのまま使えます。一方で「軽くて速い」という紹介は半分しか当たっていません。後述する2フィールドの実測では、メモリ使用量は辞書の約半分、生成時間は辞書リテラルの約4倍でした。また、json.dumps() で直接JSON化するとフィールド名が消えます。この記事では定義方法と専用API、辞書からの変換、typing.NamedTuple との違いを整理し、Python 3.14.6 と 2026年10月に公開された 3.15.2 で測った速度・メモリの差と、3.15 で使えなくなった書き方までをまとめます。
まとめ
- namedtuple は tuple のサブクラスを作る関数。インスタンス辞書を持たないため、2フィールドなら辞書の半分(112バイト対224バイト)で済む。
- 速さは選ぶ理由にならない。生成は辞書リテラルの約4倍、dataclass の約2倍。属性アクセスも dataclass の約2倍かかる(3.14.6・3.15.2 とも同じ傾向)。
- 選ぶ理由はタプル互換(アンパック・インデックス・タプルを受け取る既存APIへの受け渡し)と、書き換えられないこと。
- 型注釈を書くなら typing.NamedTuple のクラス構文。
class E(NamedTuple)と直接定義したクラスのメソッドではsuper()が 3.11 以前から使えない(3.14 でエラーが TypeError に変わった)。 - Python 3.15 で
NamedTuple("NT", x=int)のキーワード引数構文と、フィールド省略のNamedTuple("NT")が削除された。 - 3.15 からは namedtuple のサブクラスに任意の
__slots__を書ける(3.14 までは TypeError)。 - 値の書き換えやフィールド追加が多いなら dataclass、入力値の検証や型変換が必要なら Pydantic を選ぶ。dataclassの型注釈だけでは入力値は検証されない。
namedtupleの基本:フィールド名で読める不変タプル
namedtuple() はクラスではなく、タプルのサブクラスを生成して返すファクトリ関数です。第1引数に型名、第2引数にフィールド名を渡します。フィールド名は ["host", "port"] のようなリストでも、空白かカンマで区切った1つの文字列でも構いません。
from collections import namedtuple
Server = namedtuple("Server", "host port role")
s = Server("10.0.1.20", 5432, "primary")
print(s.host, s[0], s.port)
# => 10.0.1.20 10.0.1.20 5432
print(s)
# => Server(host='10.0.1.20', port=5432, role='primary')
host, port, role = s # タプルなのでアンパックできる
print(s._fields)
# => ('host', 'port', 'role')
通常のタプルとの実務上の差は、s.host と s[0] のどちらでも同じ値に届くことと、print() にフィールド名が出ることです。ログを読むときに「3番目の要素は何だったか」を調べる手間がなくなります。タプルなので代入はできず、s.host = "10.0.1.21" は AttributeError: can't set attribute になります。
collections.namedtupleの定義と専用API
namedtupleの既定値と不正なフィールド名の処理
シグネチャは namedtuple(typename, field_names, *, rename=False, defaults=None, module=None) で、3つのオプションはキーワード専用です。defaults(3.7で追加)は右詰めで当てはまり、_field_defaults で確認できます。rename=True を付けると、予約語・重複・アンダースコア始まりなどの不正な名前が _1 のような位置名に置き換わります。
Server = namedtuple("Server", ["host", "port", "role"], defaults=[5432, "replica"])
print(Server("10.0.1.21"))
# => Server(host='10.0.1.21', port=5432, role='replica')
print(Server._field_defaults)
# => {'port': 5432, 'role': 'replica'}
Row = namedtuple("Row", ["id", "class", "id"], rename=True)
print(Row._fields)
# => ('id', '_1', '_2')
rename=False(既定)のまま不正な名前を渡すと、型を定義した時点で ValueError になります。メッセージは予約語なら Type names and field names cannot be a keyword: 'class'、アンダースコア始まりなら Field names cannot start with an underscore: '_x'、重複なら Encountered duplicate field name: 'a' です。アンダースコア始まりが禁止なのは、専用APIが _ 始まりの名前を使っているためです。
_replaceによる更新コピーと_asdictによる辞書変換
専用APIがアンダースコアで始まるのは、フィールド名との衝突を避けるためで、非公開という意味ではありません。値を変えた新しいインスタンスは _replace()、辞書への変換は _asdict() で作ります。
import copy
print(s._replace(role="replica"))
# => Server(host='10.0.1.20', port=5432, role='replica')
print(s.role)
# => primary ← 元のインスタンスは変わらない
print(s._asdict())
# => {'host': '10.0.1.20', 'port': 5432, 'role': 'primary'}
print(copy.replace(s, port=5433)) # 3.13以降
# => Server(host='10.0.1.20', port=5433, role='primary')
存在しないフィールドを _replace() に渡すと、3.13以降は TypeError: Got unexpected field names: ['z']、3.12までは同じ文言の ValueError です。古い版に合わせて except ValueError で捕まえているコードは、3.13へ上げると素通りします。_asdict() の戻り値は 3.8以降ふつうの dict です。3.13で追加された copy.replace() は dataclass にも使えるので、更新処理を両者で同じ書き方に揃えられます。仕様の一次情報は公式ドキュメントの collections.namedtuple です。
辞書・JSON・CSVからのnamedtuple生成
辞書から作るときは Server(**d) と書きます。ただし辞書にフィールド以外のキーが1つでもあると TypeError: Server.__new__() got an unexpected keyword argument 'user' で止まります。APIのレスポンスのように余分なキーが混ざるデータは、_fields で絞ってから渡します。
import json, csv, io
d = {"host": "db", "port": 5432, "role": "primary", "user": "app"}
s = Server(**{k: d[k] for k in Server._fields if k in d})
# ネストしたJSONを丸ごと属性アクセスにする
src = '{"db": {"host": "a", "port": 1}, "tags": ["x"]}'
cfg = json.loads(src, object_hook=lambda o: namedtuple("J", o.keys(), rename=True)._make(o.values()))
print(cfg.db.port)
# => 1
# CSVはヘッダー行をそのままフィールド名にできる
f = io.StringIO("id,name,price\n1,pen,120\n2,note,300\n")
r = csv.reader(f)
Row = namedtuple("Row", next(r), rename=True)
rows = [Row._make(x) for x in r]
print(rows[0].price, type(rows[0].price).__name__)
# => 120 str
_make() はイテラブルを1つ受け取って生成するクラスメソッドで、csv.reader の行と相性がよい書き方です。ただし値は文字列のまま入り、型変換はしません。ヘッダーに空白や予約語が含まれる場合は rename=True を付けておかないと定義時点で落ちます("unit price" と "def" は _0・_1 に置き換わる)。object_hook で作った入れ子の namedtuple は、関数の中で毎回新しい型を作っているため後述の pickle に通りません。設定ファイルの読み込みなど、プロセス内で読むだけの用途に限ってください。
typing.NamedTupleによる型注釈付きタプルの定義
クラス構文・既定値・ジェネリクス
typing.NamedTuple は collections.namedtuple の型付き版で、作られるのは同じタプルのサブクラスです。フィールドはクラス本体にアノテーションで書き、既定値は代入で付けます。既定値のないフィールドを既定値ありの後ろに置くと TypeError: Non-default namedtuple field y cannot follow default field x です。メソッドとdocstringを書ける点(3.6.1以降)と、ジェネリクスに対応する点(3.11以降)が関数形式との違いです。
from typing import NamedTuple
class Endpoint(NamedTuple):
"""APIエンドポイントの定義."""
url: str
timeout: float = 3.0
def with_timeout(self, sec: float) -> "Endpoint":
return self._replace(timeout=sec)
e = Endpoint("https://api.example.com/v1/users")
print(e.with_timeout(10.0))
# => Endpoint(url='https://api.example.com/v1/users', timeout=10.0)
# 型パラメータ構文は3.12以降。3.11では class Page(NamedTuple, Generic[T]) と書く
class Page[T](NamedTuple):
items: list[T]
total: int
print(Page[int]([1, 2, 3], 3))
# => Page(items=[1, 2, 3], total=3)
新しく書くコードならクラス構文を選びます。mypy や Pyright がフィールドの型を読めるうえ、3.15で削除されたのはキーワード引数形式だけで、クラス構文は影響を受けていません。collections.namedtuple 自体にも廃止の予定はありません。仕様は typing.NamedTuple の公式ドキュメントにあります。
NamedTupleの直接定義におけるsuper()と多重継承の制約
class E(NamedTuple) のようにtyping.NamedTupleから直接定義するクラスでは、メソッド内の super() が使えません。生成済みのEをさらに継承する通常のクラスでは使えます。3.14では定義した時点で TypeError: uses of super() and __class__ are unsupported in methods of NamedTuple subclasses になります。「3.14で使えなくなった」と書かれることがありますが、3.11.15・3.12.13・3.13.14 で同じコードを動かすと、いずれもクラス定義の時点で RuntimeError: __class__ not set defining 'E' ... が出ます。以前から動かなかった書き方を、3.14で分かりやすいエラーに変えたというのが実態です(gh-130082・3.14.0a6)。親の処理を呼びたいときは tuple.__repr__(self) のように親クラスを名指しします。collections.namedtuple を継承したクラスでは super() が普通に動くので、両者は完全には同じではありません。
もう1つの制約は多重継承です。class T(NamedTuple, Mixin) は TypeError: can only inherit from a NamedTuple type and Generic で弾かれます。NamedTupleから直接定義する際に併用できるのは Generic だけです。ただし、生成済みの名前付きタプル型をさらに継承するクラスでは、通常の継承規則に従ってミックスインを組み合わせられます。共通処理はミックスインにせず、メソッドをクラス本体に書くか関数に切り出します。
Python 3.15で変わったnamedtupleの仕様
Python 3.15.0 は2026年10月9日に公開され、10月26日の 3.15.2 が現時点の最新です(PEP 790)。namedtuple まわりには削除と緩和が1つずつ入りました。どちらも What’s New In Python 3.15 に載っています。3.15全体の変更点と上げる手順はPython 3.15の新機能|遅延インポートとUTF-8既定化、3.14から上げる手順で扱っています。
キーワード引数構文とフィールド省略の削除
3.13から非推奨だった NamedTuple("NT", x=int) が、3.15で使えなくなりました(gh-133817)。3.15の Lib/typing.py では定義が def NamedTuple(typename, fields, /) になり、キーワード引数の受け口そのものがありません。フィールドを省略する書き方も同時に通らなくなっています。3.14.6 と 3.15.2 で同じコードを動かした結果が次のとおりです。
from typing import NamedTuple
NT = NamedTuple("NT", x=int)
# 3.14.6 => 動くが DeprecationWarning:
# Creating NamedTuple classes using keyword arguments is deprecated
# and will be disallowed in Python 3.15. ...
# 3.15.2 => TypeError: NamedTuple() got an unexpected keyword argument 'x'
NamedTuple("NT") # 3.15.2 => TypeError: NamedTuple() missing 1 required positional argument: 'fields'
NamedTuple("NT", None) # 3.15.2 => TypeError: 'NoneType' object is not iterable
# 書き換え先1: 関数形式(フィールドは (名前, 型) のリスト)
Point = NamedTuple("Point", [("x", int), ("y", int)])
# 書き換え先2: クラス構文
class Point2(NamedTuple):
x: int
y: int
# フィールド0個の型: class Empty(NamedTuple): pass か NamedTuple("Empty", [])
3.14までの DeprecationWarning は、__main__ で直接実行したときは既定で表示されますが、import されたモジュールの中で呼んでいると既定では出ません。3.15へ上げる前に python -W error::DeprecationWarning かテストの警告設定で洗い出しておくと、本番で初めて TypeError を見ることを防げます。
tuple派生クラスにおける__slots__制約の緩和
緩和されたのは継承側です。3.14までは namedtuple を継承したクラスに空でない __slots__ を書くと、定義した時点で失敗しました。3.15では tuple 派生クラスに任意の __slots__ を書けます(gh-41779)。
from collections import namedtuple
P = namedtuple("P", "x y")
class Cached(P):
__slots__ = ("_h",)
c = Cached(3, 4)
c._h = 5.0
# 3.14.6 => TypeError: nonempty __slots__ not supported for subtype of 'P'
# 3.15.2 => 動く。hasattr(c, "__dict__") は False のまま
これまでは __slots__ = () を指定すると追加属性を保存できませんでしたが、継承先でこの指定を省けば、インスタンス辞書に計算結果を保存できました。3.15の変更は、インスタンス辞書を持たせずに追加スロットへ保存できるようになったことです。ただし同じコードは3.14以前で定義時に落ちるので、サポート範囲に3.14以前を含むライブラリではまだ使えません。
match文とpickleでの扱い
namedtupleのクラスパターンと位置引数による分解
namedtuple には __match_args__ にフィールド名が入っているため、3.10以降の match 文でクラスパターンを位置で書けます。素のタプルも同じ match に流せるので、タプルと namedtuple が混在するデータの振り分けに向きます。
S = namedtuple("S", "host port")
print(S.__match_args__)
# => ('host', 'port')
def route(v):
match v:
case S(h, 5432):
return f"PostgreSQL {h}"
case S(host=h, port=p):
return f"other {h}:{p}"
case (a, b):
return f"tuple {a},{b}"
print(route(S("db", 5432)), route(S("db", 3306)), route(("x", 1)))
# => PostgreSQL db other db:3306 tuple x,1
パターンの順番には注意が要ります。namedtuple はタプルでもあるので、case (a, b) を先に書くと S もそこで捕まります。型で分けたい分岐は、シーケンスパターンより上に置きます。
pickleによる復元と型の名前解決条件
pickle はインスタンスを「モジュール名+型名」で保存し、読み込み時にその名前で型を探します。そのため namedtuple の型をモジュール直下に、変数名と型名を揃えて定義しておけば普通に往復できます。関数内で生成し、対応する名前でモジュールに公開していない型は見つからず、PicklingError: Can't pickle <class '__main__.L'>: it's not found as __main__.L で止まります。先ほどの object_hook で作った入れ子の型も同じ理由で失敗します。multiprocessing でプロセス間に渡す値や、キャッシュに保存する値に namedtuple を使うなら、型はモジュール直下で1回だけ定義します。module 引数(3.6で追加)は型の __module__ を指定するだけなので、その名前で型がモジュールに置かれていなければ pickle は通りません。
dict・dataclassとの実測比較(3.14.6・3.15.2)
測定環境は macOS(Intel Core i9-9880H)上の CPython 3.14.6 と 3.15.2(どちらも python-build-standalone 版)です。測定スクリプトは2フィールドの型を対象にしています。
メモリ:200,000インスタンスあたりの実測値
namedtuple が軽い理由は、インスタンスごとの __dict__ を持たないことです。200,000個を生成し、tracemalloc のトレース増分をインスタンス数で割りました(値の整数オブジェクトとリストの参照1つ分を含む)。3回測って3回とも同じ値で、3.14.6 と 3.15.2 でも差はありませんでした。
| データ構造 | 1インスタンスあたり | dict比 |
|---|---|---|
| dataclass(slots=True) | 88 バイト | 0.39 |
| tuple | 104 バイト | 0.46 |
| namedtuple | 112 バイト | 0.50 |
| dataclass(既定) | 128 バイト | 0.57 |
| dict | 224 バイト | 1.00 |
辞書の半分で済みますが、最小は slots=True を付けた dataclass で、namedtuple はこれに24バイト負けます。sys.getsizeof() はタプルも namedtuple も同じ64を返すので、表の8バイト差は大量に確保したときの実測差として現れるものです。数百万件をメモリに並べるなら、行ごとのオブジェクトをやめて列指向の構造に寄せるほうが削減幅は大きくなります(Polarsとは?pandasとの違い・高速化の仕組みと実務での採用判断【実装目線】)。
生成と属性アクセスの速度
timeitによる5回分の実行結果の中央値です。うち3回はprocess_timeで500,000回×15反復、残る2回は既定タイマーで200,000回×40巡を測り、それぞれの最小値を集計しています。他の処理と並行して動くマシンのため、同じ操作でも回によって最大2倍ほどぶれました。絶対値ではなく、順序と倍率で読んでください。
| 区分 | 操作 | 3.14.6 | 3.15.2 |
|---|---|---|---|
| 生成 | namedtuple(位置引数) | 451 ns | 343 ns |
| 生成 | namedtuple(キーワード引数) | 643 ns | 658 ns |
| 生成 | namedtuple(_make) | 427 ns | 390 ns |
| 生成 | dataclass | 184 ns | 188 ns |
| 生成 | dataclass(slots=True) | 160 ns | 162 ns |
| 生成 | dict リテラル | 112 ns | 98 ns |
| アクセス | namedtuple(.x) | 40 ns | 39 ns |
| アクセス | namedtuple([0]) | 29 ns | 30 ns |
| アクセス | dict([‘x’]) | 26 ns | 34 ns |
| アクセス | dataclass(.x) | 20 ns | 19 ns |
各回の比を版ごとに中央値で見ると、namedtuple の生成は辞書リテラルの約4倍(回ごとの比は2.4〜5.7倍)、dataclass の約2〜2.7倍でした。キーワード引数で生成すると位置引数の約1.5〜1.9倍、属性アクセス .x は dataclass の約2倍です。回ごとの倍率には幅がありますが、namedtuple の生成がいちばん重いという順序はすべての回で同じでした。3.15.2 の位置引数生成は中央値が下がっていますが、回ごとの幅(293〜460ns)が3.14.6(404〜556ns)と重なっており、改善とは言い切れません。
生成が重いのは、__new__ が lambda _cls, x, y: _tuple_new(_cls, (x, y)) というPythonの関数として作られ、中間のタプルを組んでから tuple.__new__ を呼ぶ二段構えになっているためです(3.15の Lib/collections/__init__.py でも同じ)。100万件を組み立てるループなら、辞書より0.3秒ほど余分にかかる計算です。生成回数が支配的な処理なら辞書か dataclass、タプル互換と不変性が要るなら namedtuple、と分けて考えます。
namedtupleを避けるべき場面
JSON直列化における配列化とフィールド名の欠落
namedtuple はタプルなので、json.dumps() は配列にします。フィールド名は出力に残りません。
import json
print(json.dumps(s))
# => ["db", 5432, "primary"]
print(json.dumps(s._asdict()))
# => {"host": "db", "port": 5432, "role": "primary"}
APIのレスポンスをこのまま返すと、受け手には位置に依存した配列が届きます。フィールドを1つ挿入しただけでクライアントが壊れるので、外へ出す直前に _asdict() を挟むか、レスポンスの型に namedtuple を使わないことにします。
namedtupleの等価比較と型チェッカーの検出範囲
比較はタプルの比較なので、型名もフィールド名も見られません。
Point = namedtuple("Point", "x y")
Size = namedtuple("Size", "w h")
print(Point(1, 2) == Size(1, 2)) # => True
print(Point(1, 2) == (1, 2)) # => True
print({Point(1, 2): "p"}.get((1, 2))) # => p 辞書のキーも素のタプルと衝突する
typing.NamedTuple で型を付けた場合に、型チェッカーがどこまで拾うかを mypy 2.4.1 と pyright 1.1.414 で確かめました。Size を受け取る関数に Point を渡す誤りは両方とも検出します。Point(1, 2) == Size(1, 2) を検出したのは mypy --strict(strict_equality)だけで、mypy の既定設定と pyright の既定設定では素通りでした。class User(NamedTuple): last: str; first: str に User(*name) で姓名を逆順に流し込む誤りは、どちらも検出しません。両方とも str なので型としては正しいからです。同じ型の値を位置引数で渡す順序の誤りは、今回の型チェックでは検出できません。キーワード引数で意味を明示し、テストやレビューでも確認します。意味の違う型を取り違えたくないなら、クラスが違えば等しくならない dataclass を使います。
リストの既定値を省略したインスタンス間での共有
既定値は型に1つだけ保存され、インスタンスごとにコピーされません。
from typing import NamedTuple
class Cfg(NamedTuple):
tags: list = []
a, b = Cfg(), Cfg()
a.tags.append("x")
print(b.tags, a.tags is b.tags)
# => ['x'] True
namedtuple 自体は不変でも、中に入れたリストは変更できます。dataclass で同じ定義を書くと ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory で定義時に止まりますが、namedtuple には何のチェックもありません。既定値には () や None のような不変の値を使い、可変の値が要るなら dataclass の field(default_factory=list) に移します。
書き換え・検証が要るなら dataclass・Pydantic・SimpleNamespace
namedtuple が向くのは、フィールドが固定で、値を書き換えず、位置と名前の両方で読みたいデータです。この条件から外れるなら使いません。フィールドを1つ足すたびに位置引数で生成している箇所を全部確認することになり、Server(a, b, c, d, e) は要素が増えるほど読めなくなります。
- 書き換えたい構造体:
@dataclass(slots=True)。メモリも速度も namedtuple を上回る(上の実測表)。 - 入力の検証や型変換が要る:Pydantic のモデル(Pydanticとは?読み方・BaseModelの使い方とV1との違い【2.13対応】)。
- 「属性で読める辞書」が欲しいだけ:
types.SimpleNamespace(**d)。キーの数を決めずに作れて、後から代入もできる。 - 実体は辞書のまま型だけ付けたい:
TypedDict。
業務システムでは、画面・API・DBの間を流れるデータの型をどれにするかが、後からの改修のしやすさを左右します。データ構造の設計から相談したい場合はフルスクラッチ開発の支援内容をご覧ください。
よくある質問
namedtupleと通常のタプルの違いは何ですか?
フィールド名でアクセスできることと、print() や repr() にフィールド名が出ることです。インデックスアクセスとアンパックは通常のタプルと同じように使えます。メモリは2フィールド・200,000件の実測で112バイト対104バイトと、ほぼ変わりません。
namedtupleの値を変更するにはどうすればよいですか?
代入はできないので、s._replace(role="replica") で指定したフィールドだけを差し替えた新しいインスタンスを作ります。3.13以降は copy.replace(s, port=5433) でも同じです。値を頻繁に書き換えるなら、そもそも dataclass のほうが向いています。
typing.NamedTupleとcollections.namedtupleはどちらを使うべきですか?
新しいコードでは typing.NamedTuple のクラス構文です。型チェッカーがフィールドの型を読めて、メソッドやジェネリクスも書けます。collections.namedtuple は、CSVのヘッダーのように、型注釈なしでフィールド名を実行時に組み立てる場合に適しています。なお、typing.NamedTupleで生成済みの型をさらに継承するクラスでも super() は使えます。NamedTuple("NT", x=int) の形式は 3.15 で削除されたので使えません。
辞書からnamedtupleを作るには?逆に辞書へ変換するには?
辞書から作るときは Server(**d) です。余分なキーがあると TypeError になるため、{k: d[k] for k in Server._fields if k in d} で絞ってから渡します。namedtuple から辞書へは s._asdict() で、戻り値はふつうの dict です。
namedtupleとdataclassはどちらが速いですか?
3.14.6・3.15.2 の実測ではどちらも dataclass が速く、生成は namedtuple の約半分、属性アクセスも約半分の時間でした。メモリも slots=True の dataclass が88バイトで、namedtuple の112バイトを下回ります。namedtuple を選ぶ理由は速度ではなく、タプルとして扱えることと書き換えられないことです。