2026年6月18日

2026年6月18日

Pythonのdataclassesの使い方|field・frozen・__post_init__・asdictと注意点

Python の dataclasses は、クラスに @dataclass を付けるだけで __init__・__repr__・__eq__ を自動で作ってくれる標準ライブラリです。設定値や API の応答、ログの1行など「データを入れておくための入れ物」を、辞書より安全に、普通のクラスより短く書けます。この記事では、基本の書き方から、つまずきやすい可変デフォルト値の扱い、frozen による変更禁止、__post_init__ での検証、辞書との相互変換、Python 3.10 以降で使える slots と kw_only までを、WP-CLI の出力を扱う例で説明します。

基本の書き方

from dataclasses import dataclass


@dataclass
class Site:
    name: str
    url: str
    php_version: str = "8.3"


site = Site("ブログ", "https://example.com")
print(site)
# Site(name='ブログ', url='https://example.com', php_version='8.3')
print(site == Site("ブログ", "https://example.com"))  # True

クラス変数に型ヒントを付けて並べると、それがフィールドになります。@dataclass は次のメソッドを自動で用意します。

  • __init__:フィールドの順に引数を受け取る
  • __repr__:print() したときに中身が読める形で表示する
  • __eq__:フィールドの値がすべて等しければ == が True になる

普通のクラスで同じことをすると、self.name = name を並べた __init__ と、比較や表示のメソッドを自分で書くことになります。フィールドが増えるほど書き漏れが起きやすくなるので、「データを持つことが主な役割のクラス」は dataclass にすると見通しがよくなります。

注意したいのは、型ヒントは実行時に検査されないことです。Site(name=123, url=None) と書いてもエラーにはなりません。型の誤りは mypy などの型チェッカーで見つけるか、外部から受け取るデータで実行時の検証が必要なら Pydantic のようなライブラリを使います。

デフォルト値の順番と可変オブジェクト

dataclass でよく出る2つのエラーがあります。

1つ目は、デフォルト値のあるフィールドの後に、デフォルト値のないフィールドを書くと TypeError になることです(メッセージは non-default argument ... follows default argument のような形)。自動で作られる __init__ の引数の並びとして成り立たないためで、デフォルト値のないフィールドを先に書けば解決します。

2つ目は、リストや辞書をそのままデフォルト値にできないことです。

from dataclasses import dataclass, field


@dataclass
class Post:
    title: str
    # tags: list[str] = []   # ValueError: mutable default ... use default_factory
    tags: list[str] = field(default_factory=list)
    meta: dict[str, str] = field(default_factory=dict)

= [] と書くと、すべてのインスタンスが同じ1つのリストを共有してしまい、あるインスタンスへの追加が他にも反映される不具合につながります。dataclass はこれを防ぐために定義の時点でエラーにします。field(default_factory=list) と書けば、インスタンスを作るたびに新しいリストが用意されます。

field() には他にも便利な引数があります。

@dataclass
class DbConfig:
    host: str
    user: str
    password: str = field(repr=False)          # print しても表示しない
    port: int = 3306
    dsn: str = field(init=False, default="")   # __init__ では受け取らない

repr=False は、パスワードやトークンをうっかりログに出さないための指定です。dataclass の __repr__ は全フィールドを表示するため、秘密情報を持つフィールドには付けておくと安心です。

__post_init__ で検証と派生値の計算をする

自動生成された __init__ の直後に何か処理をしたい場合は、__post_init__ を定義します。

from dataclasses import dataclass, field


@dataclass
class DbConfig:
    host: str
    user: str
    password: str = field(repr=False)
    port: int = 3306
    dsn: str = field(init=False, default="")

    def __post_init__(self) -> None:
        if not 1 <= self.port <= 65535:
            raise ValueError(f"port が範囲外です: {self.port}")
        self.dsn = f"mysql://{self.user}@{self.host}:{self.port}"

値の範囲チェックや、他のフィールドから計算する値の設定に使います。型の変換(文字列で来た "3306" を整数にするなど)をここで行うこともできますが、変換が増えてきたら Pydantic のような専用のライブラリを検討する目安です。

frozen=True で変更できないようにする

設定値のように、作った後に書き換えてほしくないデータには frozen=True を付けます。

from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Release:
    name: str
    version: str


r = Release("my-plugin", "1.2.0")
# r.version = "1.3.0"  # dataclasses.FrozenInstanceError
r2 = replace(r, version="1.3.0")  # 一部だけ変えた新しいインスタンスを作る

frozen にしたインスタンスは、eq=True(既定)と組み合わせると __hash__ も作られ、辞書のキーや集合の要素として使えます。値を変えたいときは dataclasses.replace() でコピーを作ります。

frozen のクラスで __post_init__ からフィールドに値を入れたい場合は、self.x = ... ではエラーになるため、object.__setattr__(self, "x", value) を使います。少し特殊な書き方なので、コメントを添えておくと読む人が迷いません。

辞書や JSON と相互に変換する

WP-CLI の wp plugin list --format=json の出力を dataclass にして扱う例です。

import json
import subprocess
from dataclasses import asdict, dataclass, fields


@dataclass
class Plugin:
    name: str
    status: str
    version: str
    update: str = "none"

    @property
    def is_active(self) -> bool:
        return self.status == "active"

    @classmethod
    def from_dict(cls, data: dict) -> "Plugin":
        names = {f.name for f in fields(cls)}
        return cls(**{k: v for k, v in data.items() if k in names})


out = subprocess.run(
    ["wp", "plugin", "list", "--format=json"],
    capture_output=True, text=True, check=True,
).stdout
plugins = [Plugin.from_dict(d) for d in json.loads(out)]

for p in plugins:
    if p.is_active and p.update == "available":
        print(f"更新あり: {p.name} {p.version}")

print(json.dumps([asdict(p) for p in plugins], ensure_ascii=False, indent=2))

Plugin(**d) と直接渡すと、出力に dataclass で定義していないキーが含まれていた場合に TypeError(予期しないキーワード引数)になります。WP-CLI のバージョンによって出力される項目が増えることもあるため、fields() で定義済みのフィールドだけを取り出してから渡しています。

逆方向の asdict() は、入れ子になった dataclass やリストも再帰的に辞書へ変換します。中身をコピーしながら変換するため、非常に大きなデータを頻繁に変換すると遅くなる点は覚えておくとよいでしょう。

Python 3.10 以降の slots と kw_only

Python 3.10 で、@dataclass に次の引数が加わりました。サポートが続いているバージョンであれば使えます(2026年10月現在)。

from dataclasses import KW_ONLY, dataclass


@dataclass(slots=True, kw_only=True)
class LogEntry:
    time: str
    level: str
    message: str


entry = LogEntry(time="2026-10-01T03:00:00", level="ERROR", message="DB接続失敗")


@dataclass
class Job:
    name: str
    _: KW_ONLY
    retries: int = 3
    timeout: int = 60


job = Job("backup", timeout=300)
  • slots=True:__slots__ を使ったクラスを作り、インスタンスのメモリ使用量を抑えます。ログの行のように大量に作るデータで効果があります。定義していない属性を後から追加できなくなるため、タイプミスにも気づきやすくなります。
  • kw_only=True:すべてのフィールドをキーワード引数でしか渡せなくします。フィールドが多いクラスで、引数の順番の取り違えを防げます。
  • KW_ONLY:これより後のフィールドだけをキーワード専用にします。先頭の必須項目は位置引数で、オプションは名前付きで、という書き分けができます。

キーワード専用のフィールドは引数の並び順の制約を受けないため、前述の「デフォルト値のあるフィールドの後に、ないフィールドを書けない」問題も、継承をしたときを含めて回避しやすくなります。

まとめ

  • @dataclass は __init__・__repr__・__eq__ を自動で作る。型ヒントは実行時には検査されない
  • リストや辞書のデフォルト値は field(default_factory=...) で指定する
  • 秘密情報のフィールドには repr=False を付け、ログへの出力を防ぐ
  • 検証や派生値は __post_init__、変更禁止は frozen=True と replace() で扱う
  • 外部データの取り込みは fields() でキーを絞ってから渡し、出力は asdict() で辞書にする

お気軽にご相談ください

お見積りへ お問い合わせへ
▲