- Python
- 技術 💻
Python dataclassの使い方|既定値の罠からslotsまで実行結果で解説
dataclass(データクラス)とは、@dataclass を付けるだけで __init__・__repr__・__eq__ を自動で作ってくれる、データを入れるためのクラスを簡単に書く仕組みです。Python 3.7から標準ライブラリに入っていて、追加のインストールは不要です。
この記事では、基本の書き方から、ミュータブルな既定値の罠(実際に出るエラー)、frozen・order・slots・kw_only、__post_init__ での入力チェック、asdict・replace、継承で起きるエラーの直し方までを、Python 3.11と3.13で実際に動かした出力つきで解説します。最後に NamedTuple・TypedDict・Pydantic・attrs との比較表と使い分けもまとめます。
dataclassの使い方早見表
| やりたいこと | 書き方 | 使えるバージョン |
|---|---|---|
| 基本の定義 | @dataclass + 型注釈つきのクラス変数 |
3.7+ |
| リスト・辞書の既定値 | field(default_factory=list) |
3.7+ |
| 変更できないようにする | @dataclass(frozen=True) |
3.7+ |
| 大小比較・ソート | @dataclass(order=True) |
3.7+ |
| メモリを減らす・属性の打ち間違いを防ぐ | @dataclass(slots=True) |
3.10+ |
| キーワード引数だけで受け取る | @dataclass(kw_only=True) / _: KW_ONLY |
3.10+ |
| 生成直後に検証・計算 | def __post_init__(self): ... |
3.7+ |
| 辞書に変換 | asdict(obj) |
3.7+ |
| 一部だけ変えたコピー | replace(obj, x=1) / copy.replace(obj, x=1) |
3.7+ / 3.13+ |
| 表示・比較から外す | field(repr=False, compare=False) |
3.7+ |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python dataclassの使い方|既定値の罠からslotsまで実行結果で解説」
dataclassの基本
普通のクラスとの違い
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str = ""
u = User("Sato", 30)
print(u)
print(u == User("Sato", 30))
User(name='Sato', age=30, email='')
True
普通のクラスで同じことをするには、__init__ で代入を3行、__repr__ と __eq__ をそれぞれ書く必要があります。dataclassは、この「決まりきったコード」を型注釈から自動で作ります。
| 自動で作られるもの | 役割 | 普通のクラスだと |
|---|---|---|
__init__ |
引数を受け取ってフィールドに代入 | 自分で書く |
__repr__ |
User(name='Sato', ...) と表示 |
<__main__.User object at 0x...> と表示される |
__eq__ |
フィールドの値がすべて同じなら等しい | 同じオブジェクトのときだけ等しい |
型注釈は「チェックされない」
型注釈はフィールドを見つけるための目印で、実行時に型のチェックはされません。
@dataclass
class T:
age: int
print(T("thirty"))
T(age='thirty')
int と書いても文字列がそのまま入ります。型の誤りを見つけたいときは mypy などの型チェッカーを使うか、後で説明する __post_init__ や Pydantic で検証します。
ミュータブルな既定値の罠と default_factory
そのまま書くとエラーになる
リストや辞書を既定値にしようとすると、クラスを定義した時点で ValueError になります。
@dataclass
class Cart:
items: list = []
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
なぜ禁止されているのか
普通の関数の既定値で、同じ問題を確認できます。
def add(item, bag=[]):
bag.append(item)
return bag
print(add(1), add(2))
[1, 2] [1, 2]
既定値のリストは1つだけ作られ、すべての呼び出しで使い回されます。2回目の呼び出しに1回目の値が残ってしまうため、dataclassではこの書き方を最初からエラーにしてくれています。
field(default_factory=...) で直す
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list = field(default_factory=list)
a, b = Cart(), Cart()
a.items.append("apple")
print(a, b)
Cart(items=['apple']) Cart(items=[])
default_factory には「呼び出すと新しい値を返すもの」を渡します。インスタンスを作るたびに list() が呼ばれるので、a と b は別々のリストを持ちます。
| 既定値 | 書き方 |
|---|---|
| 空のリスト | field(default_factory=list) |
| 空の辞書 | field(default_factory=dict) |
| 初期値つきのリスト | field(default_factory=lambda: ["a", "b"]) |
| 現在時刻 | field(default_factory=datetime.now) |
なお、Python 3.11からは list・dict・set だけでなく、ハッシュできない値(unhashable)全般が既定値として拒否されるようになりました。
frozen:変更できないデータクラス
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
p.x = 10
FrozenInstanceError: cannot assign to field 'x'
frozen=True にすると、作った後にフィールドへ代入できなくなります。さらに __hash__ も作られるので、辞書のキーや集合の要素にできます。
print(hash(p) == hash(Point(1, 2)), {p, Point(1, 2)})
True {Point(x=1, y=2)}
一方、frozenでない普通のdataclassは、ハッシュできません。
TypeError: unhashable type: 'U2'
eq=True(既定)のときは、値が変わるとハッシュも変わってしまうため、__hash__ が None に設定されるからです。
frozenでも「中身」は変えられる
@dataclass(frozen=True)
class Team:
members: list = field(default_factory=list)
t = Team()
t.members.append("a")
print(t)
hash(t)
Team(members=['a'])
TypeError: unhashable type: 'list'
frozenが止めるのは「フィールドへの代入」だけです。リストの中身の変更は止められず、ハッシュも計算できません。完全に変更できないデータにしたいときは、list の代わりに tuple や frozenset を使います。
order:大小比較とソート
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
vs = [Version(1, 10, 0), Version(1, 2, 3), Version(0, 9, 9)]
print(sorted(vs))
print(Version(1, 2, 0) < Version(1, 10, 0))
[Version(major=0, minor=9, patch=9), Version(major=1, minor=2, patch=3), Version(major=1, minor=10, patch=0)]
True
order=True は、フィールドを上から順にタプルのように比べます。文字列の "1.2.0" < "1.10.0" は False になりますが、数値のフィールドで比べるので正しく並びます。
フィールドの定義順がそのまま比較の優先順位になります。別の基準で並べたいときは、比較用のフィールドを先頭に置きます。
@dataclass(order=True)
class Task:
sort_index: int = field(init=False, repr=False)
name: str
priority: int
def __post_init__(self):
self.sort_index = -self.priority # 優先度の高い順
print(sorted([Task("a", 1), Task("b", 5), Task("c", 3)]))
[Task(name='b', priority=5), Task(name='c', priority=3), Task(name='a', priority=1)]
ただし、ソートの基準が1つだけなら sorted(tasks, key=lambda t: -t.priority) の方が意図が読み取りやすい場合もあります。「このクラスには自然な順序がある」ときに order=True を使うのがおすすめです。
slots:メモリの節約と打ち間違いの防止(3.10+)
@dataclass
class A:
x: int
@dataclass(slots=True)
class B:
x: int
a, b = A(1), B(1)
a.y = 2 # 通る
b.y = 2 # エラー
Python 3.13での実行結果です。
AttributeError: 'B' object has no attribute 'y' and no __dict__ for setting new attributes
| 比較 | slotsなし | slots=True |
|---|---|---|
| 未定義の属性への代入 | 通ってしまう(打ち間違いに気づけない) | AttributeError |
__dict__ |
あり | なし |
1インスタンスのサイズ(sys.getsizeof、3.13) |
344バイト(本体+__dict__) |
40バイト |
大量のインスタンスを作るときや、属性名の打ち間違いを早く見つけたいときに有効です。
slots=True と引数なしの super() の注意
@dataclass(slots=True)
class S:
x: int
def show(self):
return super().__repr__()
S(1).show()
Python 3.13.7:TypeError: super(type, obj): obj (instance of S) is not an instance or subtype of type (S).
Python 3.14.0rc2:<__main__.S object at 0x7f954dd51030>
slots=True はクラスを作り直すため、3.13以前ではメソッド内の引数なしの super() がエラーになります。3.14(rc2で確認)では動きました。3.13以前で使う場合は、super(S, self) と明示するか、slotsを外します。
kw_only:キーワード引数だけで受け取る(3.10+)
from dataclasses import dataclass, KW_ONLY
@dataclass
class Opt:
host: str
_: KW_ONLY
port: int = 80
debug: bool = False
print(Opt("localhost", port=8080))
Opt("localhost", 8080)
Opt(host='localhost', port=8080, debug=False)
TypeError: Opt.__init__() takes 2 positional arguments but 3 were given
_: KW_ONLY より後のフィールドは、port=8080 のように名前を付けないと渡せません。フラグや設定値が多いクラスで、引数の順番を取り違えるミスを防げます。クラス全体をキーワード専用にするときは @dataclass(kw_only=True) です。
post_init:生成直後の検証と計算
@dataclass
class Order:
unit_price: int
quantity: int
total: int = field(init=False)
def __post_init__(self):
if self.quantity < 1:
raise ValueError(f"quantity は1以上: {self.quantity}")
self.total = self.unit_price * self.quantity
print(Order(300, 3))
Order(300, 0)
Order(unit_price=300, quantity=3, total=900)
ValueError: quantity は1以上: 0
__post_init__ は、自動生成された __init__ の最後に呼ばれます。入力値のチェックや、他のフィールドから計算する値(field(init=False))の設定に使います。
InitVar:保存しない引数を受け取る
from dataclasses import InitVar
@dataclass
class Account:
user: str
raw_password: InitVar[str]
pw_len: int = field(init=False)
def __post_init__(self, raw_password):
self.pw_len = len(raw_password)
print(Account("sato", "s3cret"))
Account(user='sato', pw_len=6)
InitVar のフィールドは __init__ の引数にはなりますが、インスタンスには保存されません。生の値を持ち続けたくないときに便利です。
asdict・astuple・replace・fields
from dataclasses import asdict, astuple, replace, fields
from datetime import date
import json
@dataclass
class Address:
city: str
@dataclass
class Person:
name: str
address: Address
born: date
pp = Person("Sato", Address("Tokyo"), date(1990, 1, 2))
d = asdict(pp)
print(d)
print(astuple(pp))
json.dumps(d)
{'name': 'Sato', 'address': {'city': 'Tokyo'}, 'born': datetime.date(1990, 1, 2)}
('Sato', ('Tokyo',), datetime.date(1990, 1, 2))
TypeError: Object of type date is not JSON serializable
asdict は入れ子のdataclassも辞書に変換しますが、date などはそのまま残ります。JSONにするには default=str などを指定します。
print(json.dumps(d, default=str, ensure_ascii=False))
{"name": "Sato", "address": {"city": "Tokyo"}, "born": "1990-01-02"}
| 関数 | 用途 |
|---|---|
asdict(obj) |
辞書に変換(入れ子も再帰的に。値は深いコピー) |
astuple(obj) |
タプルに変換 |
replace(obj, name="Suzuki") |
一部だけ変えた新しいインスタンスを作る(frozenでも使える。__post_init__ も再度呼ばれる) |
copy.replace(obj, ...) |
3.13+ の標準の書き方。dataclassにも使える |
fields(Cls) |
フィールドの一覧(名前・型・既定値など)を取得 |
継承するときの注意
既定値ありのフィールドの後ろに、既定値なしは置けない
@dataclass
class Base:
id: int
note: str = ""
@dataclass
class Child(Base):
price: int
Python 3.11:TypeError: non-default argument 'price' follows default argument
Python 3.13:TypeError: non-default argument 'price' follows default argument 'note'
継承したフィールドは親のフィールドの後ろに並ぶため、__init__(self, id, note="", price) という書けない形になり、エラーになります(3.13ではどのフィールドが原因かがメッセージに出ます)。
kw_only=True で直せます。
@dataclass(kw_only=True)
class Child2(Base):
price: int
print(Child2(1, price=100))
Child2(1, 100)
Child2(id=1, note='', price=100)
TypeError: Child2.__init__() missing 1 required keyword-only argument: 'price'
Child2(1, 100) では、100が親の note に入り、price が足りないというエラーになります。子クラスで追加するフィールドをキーワード専用にすると、並び順の問題がなくなります。
親と子は「等しくならない」
@dataclass
class Pt:
x: int
@dataclass
class Pt3(Pt):
z: int = 0
print(Pt(1) == Pt3(1))
False
自動生成の __eq__ は、クラスが完全に同じときだけフィールドを比べます。
3.13からの小さな変更:NaNの比較
@dataclass
class M:
v: float
n = float("nan")
print(M(n) == M(n))
Python 3.11.15:True
Python 3.13.7:False
3.13で __eq__ がタプル比較からフィールドごとの比較に変わったためです(公式ドキュメントに記載あり)。NaNを持つデータを比較するテストは、バージョンによって結果が変わることがあります。
NamedTuple・TypedDict・Pydantic・attrsとの比較
同じ定義に、age として文字列 "30" を渡したときの結果です(Python 3.13、Pydantic 2.13.5、attrs 26.1.0)。
NamedTuple: NT(name='Sato', age='30')
TypedDict : {'name': 'Sato', 'age': '30'}
dataclass : DC(name='Sato', age='30')
attrs : TypeError: ("'age' must be <class 'int'> (got '30' that is a <class 'str'>)." ...
pydantic : PM(name='Sato', age=30)
"thirty" を渡すと、Pydanticは次のようにどの項目がなぜ不正かを返しました。
1 validation error for PM
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='thirty', input_type=str]
| 項目 | dataclass | NamedTuple | TypedDict | attrs | Pydantic |
|---|---|---|---|---|---|
| 提供元 | 標準 | 標準 | 標準 | 外部 | 外部 |
| 実体 | クラス | タプル | ただの辞書 | クラス | クラス |
| 変更 | できる(frozenで禁止) | できない | できる | できる(frozen可) | できる(frozen可) |
| 実行時の型チェック | なし | なし | なし | validatorで指定 | あり(変換も) |
位置で取り出し x[0] |
できない | できる | できない | できない | できない |
| JSONとの変換 | 自分で | 自分で | そのまま | 別ライブラリ | 標準機能 |
| 向いている場面 | アプリ内部のデータ | 軽い不変の組 | 既存の辞書・JSONに型を付ける | 高機能なクラスを多く作る | 外部入力の検証、API・設定 |
判断の目安
| 状況 | おすすめ |
|---|---|
| アプリの内部で受け渡すデータ | dataclass |
| 外部(API・フォーム・設定ファイル)からの入力を検証したい | Pydantic |
| JSONの辞書をそのまま扱いつつ、型チェッカーの補完がほしい | TypedDict |
| 小さな不変の組で、タプルとしても使いたい | NamedTuple |
| dataclassより細かい検証や変換を、多くのクラスで使いたい | attrs |
「信用できない入力の境界ではPydantic、内側ではdataclass」と分けると、依存を増やさずに安全性を保てます。
よくある失敗と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
mutable default ... use default_factory |
リスト・辞書を既定値に直接書いた | field(default_factory=list) |
non-default argument ... follows default argument |
既定値ありの後に既定値なし(継承で起きやすい) | 並べ替える、または kw_only=True |
unhashable type |
frozenでないdataclassを集合・辞書キーに使った | frozen=True(中身もtupleなどに) |
| frozenなのに中身が変わる | リストのフィールドを変更した | tuple / frozenset を使う |
| 型と違う値が入る | 型注釈は実行時にチェックされない | __post_init__ で検証、またはPydantic |
json.dumps でエラー |
asdict 後も date 等が残る |
default=str など |
slotsで super() がエラー |
3.13以前の制約 | super(Cls, self) と明示 |
よくある質問
Q. dataclassと普通のクラスは、どう使い分けますか?
A. 主な役割が「データを持つこと」ならdataclassが向いています。振る舞い(メソッド)が中心で、__eq__ などが不要なクラスは普通のクラスで十分です。dataclassにもメソッドは自由に追加できます。
Q. frozen=True にすると遅くなりますか?
A. 代入を禁止する仕組みのため、__init__ がわずかに遅くなると公式ドキュメントに書かれています。多くのアプリでは気にならない差なので、「変わらないことが保証される」利点を優先するとよいでしょう。
Q. slots=True は常に付けた方がいいですか?
A. メモリと打ち間違い防止の利点がありますが、後から属性を追加するライブラリや、3.13以前の引数なし super() とは相性がよくありません。インスタンスを大量に作るクラスから試すのがおすすめです。
Q. Pydanticがあれば、dataclassは不要ですか? A. Pydanticは検証と変換が強力な分、外部ライブラリへの依存が増えます。入力の検証が必要な境界はPydantic、内部のデータ構造は標準のdataclassと使い分けると、それぞれの良さを活かせます。
まとめ
- dataclassは、
@dataclassだけで__init__・__repr__・__eq__を自動生成する標準の仕組み - リスト・辞書の既定値は
field(default_factory=list)。直接書くと定義時にValueError frozen=Trueで代入禁止+ハッシュ可能に。ただし中身のリストは変更できるorder=Trueはフィールドの定義順で比較、slots=Trueは省メモリと打ち間違い防止- 継承で起きる
non-default argumentエラーはkw_only=Trueで解消 - 型注釈は実行時にチェックされない。検証は
__post_init__、外部入力はPydantic - 内部のデータはdataclass、境界の入力はPydantic、辞書のまま扱うならTypedDict
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python dataclassの使い方|既定値の罠からslotsまで実行結果で解説」
株式会社RJCでは、Pythonの標準機能を活かして、読みやすく壊れにくいコードを書く工夫を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。