Sổ tay kỹ thuật

Python type hints và Pydantic v2

cheatsheet#python#pydantic#typing· cập nhật 2026-11-06

Tra nhanh cú pháp type hint hiện đại (Python 3.12+), TypedDict, Protocol và cách khai báo model, field, validator, serialize trong Pydantic v2.

Sửa trang này
Mục lục

Cú pháp dưới cho Python ≥ 3.12 và Pydantic 2.x (đã chạy thử với Python 3.12, Pydantic 2.14, mypy --strict). Bảng đổi tên API v1 → v2 xem ở FastAPI cơ bản; cấu hình mypy xem ruff và mypy.

Type hints cơ bản

Mục đích Cú pháp
Collection built-in (không cần typing.List) list[str], dict[str, int], tuple[str, int], tuple[int, ...]
Có thể None str | None = None
Một trong vài giá trị cố định Literal["iosxr", "junos"]
Alias kiểu (3.12) type Vlan = int
Generic function (3.12) def first[T](xs: Sequence[T]) -> T: ...
Tham số nhận iterable / mapping (chấp nhận rộng) from collections.abc import Iterable, Mapping, Sequence
Hàm làm tham số Callable[[str, int], bool]
Generator Iterator[str]
Hằng không được gán lại MAX_RETRY: Final = 3
Method trả về chính class (3.11) def clone(self) -> Self: ...
Gắn metadata vào kiểu Annotated[int, Field(ge=1)]
Báo mypy nhánh không thể xảy ra assert_never(x) trong nhánh else của match/if
Ép kiểu cho type checker (không đổi runtime) cast(str, value)

TypedDict và Protocol

from typing import NotRequired, Protocol, TypedDict, runtime_checkable
 
class Iface(TypedDict):          # dict có key cố định, ví dụ JSON từ API
    name: str
    mtu: int
    descr: NotRequired[str]      # key có thể vắng
 
@runtime_checkable
class Driver(Protocol):          # "duck typing" có kiểm tra: class nào có get_facts() là hợp lệ
    def get_facts(self) -> dict[str, str]: ...
 
def collect(drv: Driver) -> dict[str, str]:
    return drv.get_facts()
Dùng Khi
TypedDict Dữ liệu vẫn là dict (JSON, kết quả thư viện), chỉ cần type checker hiểu key
dataclass Object nội bộ, không cần validate input
Pydantic BaseModel Input không tin cậy: API, file YAML, env; cần validate và ép kiểu
Protocol Định nghĩa "interface" mà không bắt class kế thừa

Pydantic v2: model và field

from enum import StrEnum
from ipaddress import IPv4Address, IPv4Interface
from typing import Annotated, Literal, Self
 
from pydantic import (AfterValidator, AliasChoices, BaseModel, ConfigDict, Field,
                      SecretStr, computed_field, field_validator, model_validator)
 
class Status(StrEnum):
    UP = "up"
    DOWN = "down"
 
def check_port(v: int) -> int:
    if not 1 <= v <= 65535:
        raise ValueError("port out of range")
    return v
 
Port = Annotated[int, AfterValidator(check_port)]   # kiểu tái sử dụng được
 
class Interface(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
    name: str = Field(min_length=1)
    ip: IPv4Interface | None = None                  # "192.0.2.1/31"
    mtu: int = Field(default=1500, ge=576, le=9216)  # "9000" -> 9000 (lax mode)
    status: Status = Status.UP
    tags: list[str] = Field(default_factory=list)
 
    @field_validator("name")
    @classmethod
    def lower(cls, v: str) -> str:
        return v.lower()
 
class Device(BaseModel):
    hostname: str = Field(validation_alias=AliasChoices("hostname", "name"))
    platform: Literal["iosxr", "junos"]
    mgmt_ip: IPv4Address
    ssh_port: Port = 22
    password: SecretStr | None = None                # in ra thành '**********'
    interfaces: list[Interface] = []
 
    @model_validator(mode="after")
    def no_telnet(self) -> Self:                     # kiểm tra chéo nhiều field
        if self.ssh_port == 23:
            raise ValueError("telnet port not allowed")
        return self
 
    @computed_field  # type: ignore[prop-decorator]
    @property
    def fqdn(self) -> str:
        return f"{self.hostname}.example.net"
Mục đích Cú pháp
Cấm field lạ / bỏ qua field lạ ConfigDict(extra="forbid") / extra="ignore" (mặc định)
Không tự ép kiểu ("1" không thành 1) ConfigDict(strict=True)
Model bất biến (hashable) ConfigDict(frozen=True)
Đọc từ object ORM/attribute ConfigDict(from_attributes=True)
Giá trị mặc định là list/dict mới Field(default_factory=list)
Ràng buộc số / chuỗi Field(ge=1, le=4094), Field(min_length=1, pattern=r"^[a-z0-9-]+$")
Tên field trong JSON khác tên Python Field(alias="hostName") (chỉ khi validate: validation_alias=)
Validator một field (chạy sau khi ép kiểu) @field_validator("name") + @classmethod
Validator trước khi ép kiểu @field_validator("tags", mode="before")
Validator nhiều field @model_validator(mode="after") trả về self
Field tính toán, có trong dump @computed_field + @property

Validate và serialize

Mục đích Lệnh
Từ dict / từ chuỗi JSON Device.model_validate(data) / Device.model_validate_json(raw)
Ra dict (giữ kiểu Python) d.model_dump()
Ra dict chỉ gồm kiểu JSON (IP thành str) d.model_dump(mode="json")
Bỏ field None / chỉ lấy vài field d.model_dump(exclude_none=True) / d.model_dump(include={"hostname"})
Ra chuỗi JSON d.model_dump_json(indent=2)
Copy và sửa vài field d.model_copy(update={"hostname": "pe2"})
Validate kiểu không phải model (list[...]) TypeAdapter(list[Interface]).validate_python(items)
JSON Schema (cho tool/LLM, OpenAPI) Device.model_json_schema()
Danh sách field Device.model_fields.keys()
Lấy secret thật d.password.get_secret_value()
from pydantic import ValidationError
 
try:
    Device(hostname="x", platform="eos", mgmt_ip="1.2.3", ssh_port=0)
except ValidationError as e:
    for err in e.errors():                 # 3 lỗi được gom trong một lần
        print(err["loc"], err["type"], err["msg"])
# ('platform',) literal_error ...
# ('mgmt_ip',) ip_v4_address ...
# ('ssh_port',) value_error ...

Mặc định Pydantic chạy lax mode: "9000" thành 9000, "true" thành True. Điều này tiện khi đọc YAML/env, nhưng nếu cần bắt lỗi kiểu chặt (vd input từ API công khai) thì bật strict=True cho model hoặc từng field Field(strict=True).

mypy báo Decorators on top of @property are not supported ở @computed_field: đây là hạn chế đã biết, thêm # type: ignore[prop-decorator] như ví dụ trên.