在 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
![Python 如何用pandas.Series.nsmallest() 找到n個與target差距最小的index?再從中找到距離idxmax最近的index?避免誤抓sidelobes的index? targetIdx = (serMean-target_value).abs().nsmallest(n).index.tolist() ;Series切片: .loc[標籤名1:標籤名2] (會含標籤名2) ; .iloc[位置1:位置2] (不含位置2) Python 如何用pandas.Series.nsmallest() 找到n個與target差距最小的index?再從中找到距離idxmax最近的index?避免誤抓sidelobes的index? targetIdx = (serMean-target_value).abs().nsmallest(n).index.tolist() ;Series切片: .loc[標籤名1:標籤名2] (會含標籤名2) ; .iloc[位置1:位置2] (不含位置2)](https://i2.wp.com/savingking.com.tw/wp-content/uploads/2023/02/20230222082954_53.png?quality=90&zoom=2&ssl=1&resize=350%2C233)



![Python如何寫入docx文件? from docx import Document ; doc = Document() ; table = doc.add_table(rows=5, cols=3) ; table.cell(r,c).text = str(tabs[r][c]) ; doc.add_heading ; p = doc.add_paragraph ; p.add_run ; doc.add_picture ; 使用wordPad開啟會少最後一個row,可以用免費的LibreOffice Python如何寫入docx文件? from docx import Document ; doc = Document() ; table = doc.add_table(rows=5, cols=3) ; table.cell(r,c).text = str(tabs[r][c]) ; doc.add_heading ; p = doc.add_paragraph ; p.add_run ; doc.add_picture ; 使用wordPad開啟會少最後一個row,可以用免費的LibreOffice](https://i0.wp.com/savingking.com.tw/wp-content/uploads/2022/09/20220914154313_30.jpg?quality=90&zoom=2&ssl=1&resize=350%2C233)

![Python:如何將folder_path & file_name合併為file_path? fpath = os.path.join (folder , fname) #不需要[ ]包覆folder,fname; fpath1 = “\\”.join( [folder , fname] ) #需要[ ] 包覆folder,fname ; 反過來講,file_path如何拆分為folder_path & file_name? os.path.dirname() ; os.path.basename() ; file_name如何拆分為主檔名與副檔名os.path.splitext() #split(分裂) ext Python:如何將folder_path & file_name合併為file_path? fpath = os.path.join (folder , fname) #不需要[ ]包覆folder,fname; fpath1 = “\\”.join( [folder , fname] ) #需要[ ] 包覆folder,fname ; 反過來講,file_path如何拆分為folder_path & file_name? os.path.dirname() ; os.path.basename() ; file_name如何拆分為主檔名與副檔名os.path.splitext() #split(分裂) ext](https://i2.wp.com/savingking.com.tw/wp-content/uploads/2023/07/20230717184401_87.png?quality=90&zoom=2&ssl=1&resize=350%2C233)
![Excel TQC考題202: 快樂小學學生名冊,自訂格式:0″公斤”;[紅色]”減”0″公斤”;[藍色]”完美身材” Excel TQC考題202: 快樂小學學生名冊,自訂格式:0″公斤”;[紅色]”減”0″公斤”;[藍色]”完美身材”](https://i2.wp.com/savingking.com.tw/wp-content/uploads/2022/04/20220410142405_70.png?quality=90&zoom=2&ssl=1&resize=350%2C233)

近期留言