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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?