Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel

加入好友
加入社群
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

在 Python 開發中,
我們經常需要描述一組固定結構的資料,
例如:

  • 使用者資料
  • API 請求與回應
  • 設定檔
  • 測試結果
  • 資料庫紀錄
  • JSON 文件

Python 提供多種方式處理這類資料,其中最常見的包括:

@dataclass
TypedDict
Pydantic BaseModel

from dataclasses import dataclass  
from typing import TypedDict
from pydantic import BaseModel

這三種工具看起來都可以定義欄位與型別,
但實際用途並不相同。


一、先看三者的核心差異

Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

看表前先記住三件事(細節後面各章都會展開):

  1. @dataclass 與 pydantic.BaseModel 是「同類」:兩者都是 Python 類別,實例都是物件, 存取方式都是 user.name。差別不在「是不是物件」,而在 pydantic.BaseModel 多了執行期驗證
  2. TypedDict 不是新的執行期型別:它只是「加在字典上的靜態契約」,只存在於型別檢查器 (mypy / Pyright)眼中;你實際拿在手上的資料,執行期就是一個普通 dict
  3. 命名層級不同@dataclassTypedDict 是標準庫的語言構造名;而「Pydantic」是第三方套件名, 你實際 import 並繼承的類別是 pydantic.BaseModelfrom pydantic import BaseModel class User(BaseModel): # ← 對應 @dataclass / TypedDict 的具體構造 name: str

(本文描述「模型/構造」時一律用 pydantic.BaseModel,只在指套件本身,或其版本世代 〔Pydantic 1.x / 2.x,本文以 v1 / v2 簡稱〕時才用「Pydantic」。)

可以先用三句話理解:

TypedDict:描述「這個 dict 應該有哪些欄位」的靜態型別標註。
dataclass:這是一個具有欄位的 Python 物件。
pydantic.BaseModel:這是一個會驗證輸入資料的 Python 物件。

二、dataclass:建立簡潔的資料物件

1. 傳統 Python 類別的寫法

如果不用 dataclass,可能要手動撰寫 __init__

class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

建立物件:

user = User(
    name="Alice",
    age=30
)

這種寫法沒有錯,但欄位多時會產生大量重複程式碼。


2. 使用 dataclass

from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int

Python 會自動幫你產生初始化方法:

user = User(
    name="Alice",
    age=30
)

print(user)
# 輸出User(name='Alice', age=30)

也會自動提供適合的:

  • __init__
  • __repr__
  • __eq__

因此,dataclass 非常適合用來表示「主要由資料組成」的類別。


3. dataclass 的型別標註不是驗證

這是非常重要的觀念:

from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int

雖然 age 標註為 int,但 Python 預設不會阻止錯誤型別:

user = User(
    name="Alice",
    age="thirty"
)

print(user.age)
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

也就是說:

dataclass 的型別標註主要提供給 IDE、mypy、Pyright 等
靜態分析工具使用,預設不會在執行時驗證資料。


4. dataclass 的預設值

from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int = 0

使用:

user = User(name="Alice")

print(user.age)
# 輸出0

5. 可變預設值要使用 default_factory

無法直接這樣寫:

from dataclasses import dataclass


@dataclass
class User:
    tags: list[str] = []
# ValueError: mutable default <class 'list'> for field tags is not allowed: 
# use default_factory

⚠️ 這段程式根本無法載入@dataclass 在類別定義的當下就會直接丟出 ValueError

ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory

它擋下的判斷依據是「不可雜湊(unhashable)的預設值」,而 listdictset 正好都不可雜湊(不可雜湊通常就代表可變物件),所以拿它們當預設值一定會被擋下。

作為對照,下面是傳統類別中真正會發生「被共用」的錯誤寫法
dataclass 正是為了擋住這種陷阱,才直接禁止不可雜湊預設值):

class BadUser:
    tags: list[str] = []  # 類別屬性所有實例共用同一個 list


a = BadUser()
b = BadUser()
a.tags.append("Python")
print(b.tags)  # ['Python'] ← b 也被影響了這才是共用陷阱
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

dataclass 的正確寫法:

from dataclasses import dataclass, field


@dataclass
class User:
    name: str
    tags: list[str] = field(default_factory=list)

每次建立 User 時,都會建立一個新的清單:

user1 = User(name="Alice")
user2 = User(name="Bob")

user1.tags.append("Python")

print(user1.tags)
print(user2.tags)
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

除了 list,也可以使用:

field(default_factory=dict)
field(default_factory=set)

或自己的函式:

def default_tags() -> list[str]:
    return ["new"]


@dataclass
class User:
    tags: list[str] = field(
        default_factory=default_tags
    )
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

6. dataclass 可以包含方法

dataclass 不只是資料容器,也可以包含商業邏輯或操作方法:

from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int

    def is_adult(self) -> bool:
        return self.age >= 18

使用:

user = User(
    name="Alice",
    age=30
)

print(user.is_adult())
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

這是 dataclass 與 TypedDict 的重要差異之一。


7. 不可變的 dataclass

如果希望物件建立後不能修改,可以使用 frozen=True

from dataclasses import dataclass


@dataclass(frozen=True)
class Point:
    x: int
    y: int

以下操作會失敗:

point = Point(10, 20)
point.x = 100
# FrozenInstanceError: cannot assign to field 'x'

因為 frozen dataclass 不允許修改欄位,
會丟出 FrozenInstanceError: cannot assign to field ‘x’

frozen=True 擋下的不只是「修改既有欄位」——它會攔截所有對實例的屬性設定與刪除:

Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

適合用於:

  • 座標
  • 設定值
  • 常數資料
  • 希望具有不可變特性的值物件

三、TypedDict:描述字典的結構

1. TypedDict 是什麼?

TypedDict 用來描述一個字典應該包含哪些 key,
以及每個 key 的資料型別。
它是一種靜態型別標註——只在型別檢查器眼中生效,
執行期並不會建立新的資料型別,你操作的資料仍然是普通 dict

from typing import TypedDict


class UserDict(TypedDict):
    name: str
    age: int

建立資料:

user: UserDict = {
    "name": "Alice",
    "age": 30
}

存取資料:

print(user["name"])
print(user["age"])
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

2. TypedDict 實際上仍然是普通字典

print(type(user))
# <class 'dict'>

TypedDict 的實例就是普通的 dict,執行期不會產生一個獨立的類別實例。它主要提供的是型別提示,讓 IDE 與靜態分析工具知道這個字典應該長什麼樣子。

一個容易誤會的細節:連用「建構子」建出來的也是純 dict

即使你把 UserDict 當成類別來呼叫,得到的仍然是普通 dict,而不是一個 UserDict 實例;而且執行期完全不會驗證欄位型別:

from typing import TypedDict


class UserDict(TypedDict):
    name: str
    age: int


u = UserDict(name=123, age="thirty")   # 故意給錯型別

print(type(u))          # <class 'dict'>  ← 不是 UserDict
print(u)                # {'name': 123, 'age': 'thirty'}執行期照收不誤
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

UserDict.__mro__ 雖然顯示 (UserDict, dict, object),看似 dict 的子類, 但那只是 typing 的特殊構造;UserDict(...) 建出來的物件其 type() 永遠是 dict。 換句話說,TypedDict 的角色是「靜態描述」,不是「執行期型別」。

3. TypedDict 不會自動驗證

from typing import TypedDict


class UserDict(TypedDict):
    name: str
    age: int


user: UserDict = {
    "name": 123,
    "age": "thirty"
}

Python 預設不會立刻報錯。

但是,如果使用 mypy 或 Pyright,
靜態檢查工具可能會提出錯誤:

name 應該是 str,但目前是 int
age 應該是 int,但目前是 str

因此:

TypedDict 是靜態型別提示,不是執行時資料驗證工具。


4. 必填欄位與非必填欄位

預設情況下,TypedDict 的欄位都是必填:

from typing import TypedDict


class UserDict(TypedDict):
    name: str
    age: int

以下資料不完整:

user: UserDict = {
    "name": "Alice"
}

靜態分析工具會指出缺少 age

如果某些欄位可以省略,可以使用 NotRequired

from typing import NotRequired, TypedDict


class UserDict(TypedDict):
    name: str
    age: int
    email: NotRequired[str]

此時以下資料是合法結構:

user: UserDict = {
    "name": "Alice",
    "age": 30
}

5. 全部欄位都可以省略

可以使用:

# %%
from typing import TypedDict


class UserUpdate(TypedDict, total=False):
    name: str
    age: int
    email: str

這代表所有欄位都是可選的:

# %%
update: UserUpdate = {
    "email": "alice@example.com"
}
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

6. TypedDict 適合 JSON

JSON 的本質就是字典、清單、字串、數字、布林值與 null

因此,以下資料很適合用 TypedDict 描述:

from typing import TypedDict


class ApiResponse(TypedDict):
    success: bool
    message: str
    data: dict

使用:

response: ApiResponse = {
    "success": True,
    "message": "OK",
    "data": {}
}

適合的場景包括:

  • API 回應
  • 設定資料
  • JSON 文件
  • 字典格式的查詢結果
  • 不需要方法的資料結構

四、pydantic.BaseModel:具有執行時驗證的資料模型

1. 基本用法

先安裝:

pip install pydantic

建立模型:

from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int

使用:

user = User(
    name="Alice",
    age=30
)

print(user.name)
print(user.age)
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

pydantic.BaseModel 的物件存取方式和 dataclass 類似:

user.name

2. pydantic.BaseModel 會驗證資料

from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int


user = User(
    name="Alice",
    age="thirty"
)
#ValidationError: 1 validation error for User
age
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='thirty', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/int_parsing

因為 "thirty" 無法轉換成整數,pydantic.BaseModel 會產生 ValidationError

ValidationError: 1 validation error for User
age
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='thirty', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/int_parsing

加入好友
加入社群
Python 資料模型完整教學:dataclass、TypedDict 與 Pydantic 的選擇與比較; from dataclasses import dataclass ; from typing import TypedDict ; from pydantic import BaseModel - 儲蓄保險王

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡

You may also like...

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *