How Python structures data set?
TypedDict, namedtuple, Dataclass
How Python structures data?
TypedDict, namedtuple, Dataclass
TL;DR
**TypedDict**- 給 dictionary 的 Type Annotation (not data container)- 搭配 Type Checker 檢查
- 提供 value type info 與 Type Hint 效果。
**namedtuple**- 透過 named attributes access 的方式取得資料的 tuple- 最輕量的 data container,空間優化首選
**dataclass- 快速搭建 class** schema blueprint- 彈性、擴充性最好的 data container
- 其他提供 schema + validation + serialization 功能的 third-party packages: attrs, msgspec, marshmallow, pydantic
Photo by Rubaitul Azad on Unsplash
在很多開發情境中,常常會拿到ㄧ個序列的 “資料組 data set”,而資料組的各個欄位則是由資料組的 order 來決定。
比如說,透過 csv.reader 取得的 row data,或是 sqlite3 的 cursor.execute 取得的資料列,都是用 iterable 的資料結構來儲存,而具體的欄位對應的資料,則是根據 csv header / SELECT 語句的內容來決定。
如果直接以這些原始結構進行操作,將難以理解實際上 row 資料各個順序的意涵;而解方則是透過一些 data structure 的包裝,透過 field name 的方式取代原本的 iterable order,明文定義 field 與 value 之間的關係。
以下快速介紹 3 個 Python 原生的 module,讓開發者可以快速的包裝 data row,除了增加資料的透明性,且會在 IDE 上提供友善的 Type Hint,大大優化開發上的體驗。
TypedDict
首先先從約束力最低的 TypedDict 開始說。
從 module 就可以知道,[typing.TypedDict](https://typing.python.org/en/latest/spec/typeddict.html) 其實連 data container 都不是,僅僅是一個對 dictionary 的型別注釋 (Type Annotation) 而已。
因此,一如其他 Python 的 typing,即使傳入的參數不符合 TypedDict 定義,執行上也不會報錯。
from typing import TypedDict
class User(TypedDict):
name: str
age: int
def print_user(user: User):
print(user)
user: User = {"name": "Alex"}
print_user(user)
# {'name': 'Alex'}
# 即使 user 變數不符合 User 定義,依然可以運行
TypedDict 基本上的應用情境如下:
- 搭配 Python Type Checker (如 MyPy, Pylance) 做型別檢查。
(其實
TypedDict本來就源自於 MyPy 套件型別) - 增加 dictionary data 的可讀性。
- 在編輯器中提供 Type Hint,減少 hard-coded key 的錯誤。

整理官方文件中實用的情境:
from typing import TypedDict
# ===== Basic usage =====
class Movie(TypedDict):
name: str
year: int
# Used as dictionary typing
movie_1: Movie = {"name": "Titanic", "year": 1997}
# Or use it as a dictionary constructor
movie_2 = Movie(name="Blade runner", year=1982)
# type of movie_2 is still dict
print(type(movie_2)) # <class 'dict'>
# ===== Inheritance =====
class BookBaseMovie(Movie):
based_on: str
# This class posses 3 keys: name, year, based_on
# ===== Totality =====
class Movie(TypedDict, total=False): # `total` default to True
name: str
year: int
m = Movie(name="Zootopia") # No type check error
# ===== Required and NotRequired =====
from typing import Required, NotRequired
# Use `Required` when total is False
class Movie(TypedDict, total=False):
title: Required[str]
year: int
# Use `NotRequired` when total is True
class Movie(TypedDict):
title: str
year: NotRequired[int]
# ===== Interaction with Annotated[] =====
from typing import Annotated
class Movie(TypedDict):
title: str
year: NotRequired[Annotated[int, lambda value: value > 1800]]
# OR
class Movie(TypedDict):
title: str
year: Annotated[NotRequired[int], lambda value: value > 1800]
# ===== Read-only Items =====
# NOTE: `ReadOnly` qualifier was added in 3.13
from typing import ReadOnly
class Band(TypedDict):
name: str
members: ReadOnly[list[str]]
blur: Band = {"name": "blur", "members": []}
blur["members"] = ["Damon Albarn"] # Type check error: "members" is read-only
# Can be implemented with Required / NotRequired
class OptionalName(TypedDict):
name: ReadOnly[NotRequired[str]]
namedtuple
[collections.namedtuple](https://docs.python.org/3/library/collections.html#collections.namedtuple) 可以理解成 tuple 的加強版,除了原本的 immutable, indexing 的特性外,named attribute 的功能讓開發者可以透過 dot 的方式取得資料。
當然,IDE 也會貼心的提供 type hint 做參考。
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
def print_point(p: Point):
print(p) # Point(x=1, y=1)
print(type(p)) # <class '__main__.Point'>
print(isinstance(p, tuple)) # True
print(isinstance(p, Point)) # True
print(p[0]) # 1
print(p[1]) # 2
print(p.x) # 1
print(p.y) # 2
print_point(Point(x=1, y=2))
namedtuple 的最大優點是空間效能。
在每一個 Python 物件中,會有存有一個負責管理 attributes 資料的 instance dictionaries。
除了預先在 class 中定義好的 attributes 以外,可以隨性地透過 dot 或是 setattr 的方式儲存任意 attribute。
可以透過 instance.__dict__ 來查看當前物件的 attributes:
class ClassPoint:
def __init__(self, x, y):
self.x = x
self.y = y
point = ClassPoint(x=1, y=2)
point.z = 3
print(point.__dict__)
# {'x': 1, 'y': 2, 'z': 3}
而 tuple (以及namedtuple) 透過定義 __slots__ property 來規範物件的 attributes。
雖然不能再任意增加 attribute,但因為無需生成 instance dictionary,大幅減少 memory 的使用。
(參考:https://medium.com/@apps.merkurev/dont-forget-about-slots-in-python-c397f414c490)
from collections import namedtuple
TuplePoint = namedtuple('Point', ['x', 'y'])
point = TuplePoint(x=1, y=2)
print(point.__dict__)
# raise AttributeError: 'tuple' object has no attribute '__dict__'.
用 pympler 簡單比較用 list, dictionary, tuple, namedtuple 與 class instance 來儲存 x, y, z 三點座標的 memory usage:
from pympler import asizeof
from collections import namedtuple
class ClassPoint:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
TuplePoint = namedtuple('Point', ['x', 'y', 'z'])
print(f'tuple: {asizeof.asizeof((1, 2, 3))} bytes')
print(f'list: {asizeof.asizeof([1, 2, 3])} bytes')
print(f'dict: {asizeof.asizeof(dict(x=1, y=2, z=3))} bytes')
print(f'namedtuple: {asizeof.asizeof(TuplePoint(x=1, y=2, z=3))} bytes')
print(f'instance: {asizeof.asizeof(ClassPoint(x=1, y=2, z=3))} bytes')
# tuple: 160 bytes
# list: 184 bytes
# dict: 424 bytes
# namedtuple: 160 bytes
# instance: 584 bytes
整理官方文件中實用的情境:
from collections import namedtuple
# ===== Basic usage =====
Point = namedtuple('Point', ['x', 'y'])
# Or, construct with class, which allows you to add or change functionality
class Point(namedtuple('Point', ['x', 'y'])):
__slots__ = () # prevent the creation of instance dictionaries
@property
def hypot(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __str__(self):
return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)
p = Point(x=1, y=2)
# provide concise default __repr__
print(p)
# Point(x=1, y=2)
# provide concise default dcoumentation
print(Point.__doc__)
# Point(x, y)
# yet can customize doc by making direct assignments:
Point.__doc__ += ': A 2D-point coordinate.'
Point.x.__doc__ = 'Point X (integer)'
# ===== default values =====
# `defaults` will take the values as kwargs
Account = namedtuple(
'Account',
['type', 'user', 'balance'],
defaults=['Anonymous', 0],
)
a1 = Account(type='premium', user='Terry')
a2 = Account(type='ordinary', balance=200)
print(a1) # Account(type='premium', user='Terry', balance=0)
print(a2) # Account(type='ordinary', user='Anonymous', balance=200)
# ===== namedtuple._make =====
p_data = (
(1, 2),
(3, 5),
(9, 1),
)
points = list(map(Point._make, p_data))
# list of Point obj
# ===== namedtuple._asdict =====
# useful for serialization
print(Point(x=1, y=2)._asdict()) # {'x': 1, 'y': 2}
# ===== namedtuple._replace =====
# Return a new instance of the named tuple replacing specified fields
# with new values
print(Point(x=1, y=1)._replace(x=1000)) # Point(x=1000, y=1)
# ===== namedtuple._fields =====
# Useful for introspection and for creating new named tuple types
# from existing named tuples
Point3D = namedtuple('Point3D', Point._fields + ('z',))
print(Point3D(x=1, y=2, z=3)) # Point3D(x=1, y=2, z=3)
Dataclass
dataclasses.dataclass decorator 是一個在定義 class schema 的語法糖,從文件中可以得知,在 class definition 上加上 @dataclass 後,Python 會自動幫你定義好幾個 special methods (或稱 dunder methods):
The
@dataclassdecorator will add various “dunder” methods to the class… and returns the same class that it is called on.
因此,本質上 dataclass 與 custom Class 是一樣的效果,只是省略了一些繁瑣的 function definition,同時 dataclass 也提供了一些額外的功能。
以大家最熟悉的例子:__init__ 來說,加上@dataclass 後,就不用手動一個一個在 function 定義 params、又要再一個一個 assign 到 self 上:
from dataclasses import dataclass
@dataclass
class InventoryItem:
name: str
unit_price: float
quantity_on_hand: int
# Above is equivalent to:
class NormalInventoryItem:
name: str
unit_price: float
quantity_on_hand: int
# dataclass will take care of the passed arguments
# and assignments in __init__ method
def __init__(self, name, unit_price, quantity_on_hand):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
除了基本用法,以下快速帶過幾個 dataclass 的進階應用:
dataclass parameters
先附上 dataclass parameters 的 default values,再挑幾個比較常用的來介紹:
@dataclass(
init=True,
repr=True,
eq=True,
order=True,
unsafe_hash=False,
frozen=False,
kw_only=False,
slots=False,
weakref_slot=False,
)
class Demo:
number: int
text: str
**init**: 要不要生成__init__()method**repr**: 要不要生成__repr__()method
預設的 repr 效果如下:
print(Demo(number=1, text="Python"))
# Demo(number=1, text='Python')
**eq**: 要不要生成__eq__()method (比較邏輯為將 fields value 打包成 tuple 後,跟對象 class 作比較)**order**: 要不要生成下列 comparison methods,比較方法同eq(__lt__(),__le__(),__gt__(),__ge__())**frozen: 是否要讓 instance 有 frozen 效果 (即 ”類似**” immutable 的)。 底層機制是透過__setattr__()和__delattr__()被呼叫時拋出 FrozenInstanceError 來阻止 instance 被變動,但有微小的 perfomance penalty,具體說明可看這裡。**kw_only: 是否在建構 instance 只允許透過 keyword argument 的方式傳入。我個人是偏好這個選項的,所謂 **Explicit is better than implicit.**slots**: 是否要建構__slots__attributes 來提升 space efficiency。 這個 namedtuple 有介紹到,不贅述,底下附個證明。
from dataclasses import dataclass
from pympler.asizeof import asizeof
@dataclass
class DemoOne:
number: int
text: str
@dataclass(slots=True)
class DemoTwo:
number: int
text: str
print(asizeof(DemoOne(number=1, text="2"))) # 520
print(asizeof(DemoTwo(number=1, text="2"))) # 128
default field value
像普通的 class field definition 一樣,可以在 field type annotation 旁邊加上 default values:
@dataclass
class Demo:
number: int = 0
print(Demo().number) # 0
dataclasses.**field**
dataclass 內部 Field 物件的 constructor function,可以對 attributes 做更細節的設定:
from dataclasses import dataclass, field
@dataclass
class C:
my_string: str = field(default="default")
my_list: list[int] = field(default_factory=list)
# list all field function parameters and their default values below:
field(
default=MISSING,
default_factory=MISSING,
init=True,
repr=True,
hash=None,
compare=True,
metadata=None,
kw_only=MISSING,
)
***init***: field 是否作為 dataclass init function 中的其中一個參數。 (可搭配下面介紹的 post-init 操作)***repr***: fielf 是否要顯示於 repr 字串中。 (eg. password 等機敏資料就可以設定為 False)***default*/ `default_factory`**:該 field 的預設值 / 預設值的 factory function- 兩者只能設定其一
- default_factory 需為 zero-argument callable
***compare*: field 是否要出現在 equality / comparison method 中。 (__eq__/__gt__, etc)***kw_only***: field 是否只能以 kw 的方式傳入 constructor。
Post-init processing
dataclass 提供了一個 __post__init__ function,會在 __init__ function 結束後呼叫。__post__init__ 可以搭配以下兩種情境來應用:
field(init=False),在 postinit__ 中才做處理:
@dataclass
class C:
a: float
b: float
c: float = field(init=False)
# c variable won't be passed int __init__func
def __post_init__(self):
self.c = self.a + self.b
# assign c in __post_init__
print(C(a=1, b=2)) # C(a=1, b=2, c=3)
- 搭配 dataclass 特殊的 Type Annotation
***InitVar***,表示該 field 只會在 init 階段傳入、並不會成為 dataclass 的 attribute。 dataclass 會在 __post_init__ 中傳入InitVar,方便開發者利用該參數:
@dataclass
class C:
i: int
j: int | None = None
database: InitVar[DatabaseType | None] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
Class variables
由於 dataclass 的 schema definition 方式取代了原本定義 class variable 的邏輯,如果要給 dataclass 定義 class variable 的話,需要使用 typing.ClassVar 來達成,讓 dataclass 在 init 階段忽略此欄位:
from dataclasses import dataclass
from typing import Any, ClassVar
@dataclass
class Person:
name: str
cnt: ClassVar[int] = 0
def __post_init__(self):
self.__class__.cnt += 1
p1 = Person(name="P1") # no need to pass cnt parameter
p2 = Person(name="P2")
print(p1) # Person(name='P1')
print(Person.cnt) # 2
Conclusion, and more solutions
以上介紹的三個 Python 原生模組,可以讓開發者快速的搭建 data schema structure,增加可讀性的同時也減少 typo 的機率。
不過要強調的是,TypedDict、namedtuple 和 dataclass 都沒有驗證欄位 (validation) 的效果;
雖然在 TypedDict,dataclass 可以透過 type hint 對 field type 進行標註,但即使傳入與 type hint 不合的 data type,在執行時也不會出限任何錯誤。
如果想要有 data schema / field validation / serialization 的功能,可參考以下列舉的 third-party packages:
메타데이터
- post_id
- 12dbc86f83ea
- slug
- how-python-structures-data-set-12dbc86f83ea
- url
- https://medium.com/@40243105s/how-python-structures-data-set-12dbc86f83ea
- canonical_url
- https://medium.com/@40243105s/how-python-structures-data-set-12dbc86f83ea
- author_url
- https://medium.com/@40243105s
- status
- ok
- fetched_at
- 2026-07-19 05:45:37