在 Python 開發中,
我們經常需要描述一組固定結構的資料,
例如:
- 使用者資料
- API 請求與回應
- 設定檔
- 測試結果
- 資料庫紀錄
- JSON 文件
Python 提供多種方式處理這類資料,其中最常見的包括:
@dataclass
TypedDict
Pydantic BaseModel
from dataclasses import dataclass
from typing import TypedDict
from pydantic import BaseModel這三種工具看起來都可以定義欄位與型別,
但實際用途並不相同。
一、先看三者的核心差異

看表前先記住三件事(細節後面各章都會展開):
@dataclass與pydantic.BaseModel是「同類」:兩者都是 Python 類別,實例都是物件, 存取方式都是user.name。差別不在「是不是物件」,而在pydantic.BaseModel多了執行期驗證。TypedDict不是新的執行期型別:它只是「加在字典上的靜態契約」,只存在於型別檢查器 (mypy / Pyright)眼中;你實際拿在手上的資料,執行期就是一個普通dict。- 命名層級不同:
@dataclass、TypedDict是標準庫的語言構造名;而「Pydantic」是第三方套件名, 你實際import並繼承的類別是pydantic.BaseModel:from 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: intPython 會自動幫你產生初始化方法:
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)
也就是說:
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)
# 輸出: 05. 可變預設值要使用 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)的預設值」,而 list、dict、set 正好都不可雜湊(不可雜湊通常就代表可變物件),所以拿它們當預設值一定會被擋下。
作為對照,下面是傳統類別中真正會發生「被共用」的錯誤寫法
(dataclass 正是為了擋住這種陷阱,才直接禁止不可雜湊預設值):
class BadUser:
tags: list[str] = [] # 類別屬性;所有實例共用同一個 list
a = BadUser()
b = BadUser()
a.tags.append("Python")
print(b.tags) # ['Python'] ← b 也被影響了,這才是「共用」陷阱
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)
除了 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
)
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())
這是 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 擋下的不只是「修改既有欄位」——它會攔截所有對實例的屬性設定與刪除:

適合用於:
- 座標
- 設定值
- 常數資料
- 希望具有不可變特性的值物件
三、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"])
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'} ← 執行期照收不誤
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"
}
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)
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實際使用時可以捕捉錯誤:
from pydantic import ValidationError
try:
user = User(
name="Alice",
age="thirty"
)
except ValidationError as error:
print(error)3. pydantic.BaseModel 可能會自動轉換型別
class User(BaseModel):
name: str
age: int
user = User(
name="Alice",
age="30"
)
print(user.age)
print(type(user.age))pydantic.BaseModel 通常會將:
"30"轉換為:
30
因此,pydantic.BaseModel 預設通常是「驗證並進行合理轉換」,不一定是完全禁止型別轉換。
4. 使用 Strict 型別
如果希望嚴格要求實際型別,可以使用:
from pydantic import BaseModel, StrictInt
class User(BaseModel):
name: str
age: StrictInt此時:
user = User(
name="Alice",
age="30"
)
# ValidationError: 1 validation error for User
age
Input should be a valid integer [type=int_type, input_value='30', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/int_type會驗證失敗,因為 "30" 是字串,不是整數。
常見 Strict 型別包括:
StrictStr
StrictInt
StrictFloat
StrictBool範例:
from pydantic import BaseModel
from pydantic import StrictBool, StrictInt, StrictStr
class Settings(BaseModel):
name: StrictStr
retry_count: StrictInt
enabled: StrictBool5. pydantic.BaseModel 的預設值
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = 0
tags: list[str] = Field(default_factory=list)Field(default_factory=list) 和 dataclass 的:
from dataclasses import dataclass, field
field(default_factory=list)概念相近,都是為了確保每個物件取得獨立的清單。
pydantic.Field(default_factory=list)
(大寫 Field,來自 from pydantic import Field) 和
dataclass 的 dataclasses.field(default_factory=list)
(小寫 field,來自 from dataclasses import field) 概念相近,
都是為了確保每個物件取得獨立的清單。
注意大小寫與來源不同:
pydantic.Field是大寫 F、來自pydantic;dataclasses.field是小寫 f、來自dataclasses。
兩者名字像、用途類似,但不能互換 import。
6. pydantic.BaseModel 的巢狀模型
from pydantic import BaseModel
class Address(BaseModel):
city: str
country: str
class User(BaseModel):
name: str
age: int
address: Address建立資料:
user = User(
name="Alice",
age=30,
address={
"city": "Taipei",
"country": "Taiwan"
}
)Pydantic 可以將巢狀字典轉換為 Address 物件。

7. 未宣告的額外欄位:預設會被「忽略」
這是使用 pydantic.BaseModel 做外部輸入驗證時很容易忽略的一點:Pydantic v2 預設會直接忽略未宣告的額外欄位,不會報錯。
from pydantic import BaseModel
class User(BaseModel):
name: str
user = User(name="Alice", extra_field="dropped")
print(user.model_dump())輸出(額外欄位被丟掉,但不會報錯):

如果你的目標是外部輸入的嚴格 schema(例如 API 請求、設定檔),通常會希望「收到未知欄位就直接拒絕」,此時要設定 extra="forbid":
from pydantic import BaseModel, ConfigDict, ValidationError
class User(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
try:
User(name="Alice", extra_field="x")
except ValidationError as error:
print("拒絕未知欄位:", error)extra 有三種常見設定:

五、三者實際比較
假設要描述相同的使用者資料。
1. 使用 dataclass
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int建立:
user = User(
name="Alice",
age=30
)存取:
user.name特點:
- 是真正的 Python 類別物件。
- 可以加入方法。
- 預設不驗證型別。
- 適合程式內部使用。
2. 使用 TypedDict
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int建立:
user: UserDict = {
"name": "Alice",
"age": 30
}存取:
user["name"]特點:
- 實際上是普通字典。
- 適合 JSON 與 API 資料。
- 不會執行時驗證。
- 不適合定義物件行為。
3. 使用 pydantic.BaseModel
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int建立:
user = User(
name="Alice",
age=30
)存取:
user.name特點:
- 是資料模型物件。
- 會在執行時驗證。
- 適合外部輸入與 API。
- 支援 JSON 解析與輸出。
六、資料輸出與 JSON 處理
dataclass 轉字典
from dataclasses import asdict, dataclass
@dataclass
class User:
name: str
age: int
user = User(
name="Alice",
age=30
)
data = asdict(user)
print(data)輸出:

如果要轉 JSON:
import json
json_text = json.dumps(
asdict(user),
ensure_ascii=False
)
print(json_text)TypedDict 本身就是字典
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
user: UserDict = {
"name": "Alice",
"age": 30
}可以直接使用:
import json
json_text = json.dumps(
user,
ensure_ascii=False
)pydantic.BaseModel 轉字典與 JSON
Pydantic 版本 2 使用:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
user = User(
name="Alice",
age=30
)
data = user.model_dump()
json_text = user.model_dump_json()全新
pip install pydantic預設就是 v2(2.x);只有在被 pin 舊版或維護既有 v1 專案時,才會用到下面的dict()/json()寫法。
如果是 Pydantic 版本 1,常見寫法是:
user.dict()
user.json()注意:
.dict()/.json()是 v1 的主要寫法;在 v2 它們仍可用但已 deprecated——呼叫時會發出PydanticDeprecatedSince20警告(屬DeprecationWarning),並預計在未來 v3 移除。功能上等同model_dump()/model_dump_json(),所以新程式請直接用新 API。
但新程式建議優先使用 Pydantic 2 的:
model_dump()
model_dump_json()七、如何選擇?
選擇 dataclass 的時機
適合:
- 資料主要在程式內部流動。
- 需要使用
obj.field。 - 需要加入方法。
- 需要自訂初始化或比較行為。
- 不需要自動驗證外部資料。
- 希望依賴 Python 標準函式庫。
範例:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def distance_to_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5選擇 TypedDict 的時機
適合:
- 資料本質上就是字典(例如 JSON、API 回應、設定檔)。
- 需要用
data["key"]的方式存取。 - 只想要靜態型別提示,不需要執行時驗證。
- 不需要在資料上掛方法或行為。
- 想避免額外的物件轉換成本(因為它本身就是
dict)。
範例:
from typing import TypedDict
class ApiResponse(TypedDict):
success: bool
message: str
data: dict選擇 pydantic.BaseModel 的時機
適合:
- 資料來自外部且不可信(使用者輸入、API 請求、設定檔)。
- 需要執行時驗證,確保型別與格式正確。
- 需要自動型別轉換或嚴格型別檢查。
- 需要巢狀模型、序列化與反序列化。
- 建構 Web API(例如 FastAPI)。
範例:
from pydantic import BaseModel, StrictInt
class User(BaseModel):
name: str
age: StrictInt八、一句話總結

核心判斷邏輯:
資料可信、在程式內流動 → dataclass
資料本身就是字典 / JSON → TypedDict
資料來自外部、必須驗證 → pydantic.BaseModel附錄:常見誤區速查

Pydantic 這個名字的由來與發音
由來:Python + pedantic
Pydantic 不是單一英文單字的變形,而是一個組合字:
Py(取自 Python) + dantic(取自 pedantic 後半) = Pydantic
pedantic 是什麼意思?
pedantic(形容詞)指:
- 過度注重細節的
- 過度拘泥規則的
- 學究式的、吹毛求疵的
例句:
He is very pedantic about grammar.
(他對文法非常講究,甚至有點過度拘泥細節。)
在日常英文中 pedantic 常帶負面意味(太龜毛);但用在 Pydantic 的命名上是正面的,強調「嚴謹、精確、重視資料格式、不接受錯誤輸入」——正好對應它「嚴格驗證 Python 資料」的設計理念。
發音

兩者重音都在中間的 DAN;差別只在開頭:pedantic 是「皮」、Pydantic 是「派」(因為 Py 唸成 pie)。
一句話記憶
Python + pedantic = Pydantic
(Python + 對資料細節很講究 → 派丹提克)
推薦hahow線上學習python: https://igrape.net/30afN










近期留言