Home > Blog > tech

Python Type Hints คืออะไร? สอนใช้ Type Annotation และ mypy สำหรับ Python Developer 2026

Python Type Hints mypy Guide 2026
2026-04-16 | tech | 4200 words

Python เป็นภาษา Dynamic Typing ไม่ต้องประกาศ Type ก็ใช้ได้ แต่เมื่อโปรเจกต์ใหญ่ขึ้น Code หลายพันบรรทัด ทำงานหลายคน ปัญหาที่เกิดคือ ไม่รู้ว่า Variable นี้เป็น Type อะไร Function นี้รับ Parameter อะไร Return อะไร ทำให้เกิด Bug ที่ยากหา

Type Hints (PEP 484) แก้ปัญหานี้ โดยให้คุณ ระบุ Type ลงใน Code เพื่อให้ IDE ช่วยจับ Bug, ทำ Autocomplete ได้ดีขึ้น, และให้ Type checker อย่าง mypy ตรวจสอบ Type ก่อน Run จริง

ทำไมต้อง Type Hints?

  1. จับ Bug ก่อน Runtime: mypy ตรวจพบว่าคุณส่ง int ไปให้ Function ที่รับ str ก่อนที่จะรันจริง
  2. Documentation ในตัว: อ่าน Code แล้วรู้ทันทีว่า Function รับ/Return อะไร ไม่ต้องดู Docstring
  3. IDE Support: VS Code, PyCharm ทำ Autocomplete ได้ดีขึ้นมาก เพราะรู้ Type
  4. Refactoring ง่ายขึ้น: เปลี่ยน Type → mypy บอกทุกที่ที่ต้องแก้
  5. ทีมทำงานร่วมกันง่ายขึ้น: คนอื่นอ่าน Code ของคุณแล้วเข้าใจทันที

Basic Type Annotations

# ตัวแปรพื้นฐาน
name: str = "SiamCafe"
age: int = 30
height: float = 175.5
is_active: bool = True
nothing: None = None

# Function — parameter types + return type
def greet(name: str) -> str:
    return f"Hello, {name}!"

def add(a: int, b: int) -> int:
    return a + b

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

# Function ที่ไม่ Return อะไร
def log_message(msg: str) -> None:
    print(f"[LOG] {msg}")
Note: Type Hints ไม่ได้บังคับ Runtime — Python ไม่ Error ถ้า Type ไม่ตรง แต่ mypy จะแจ้งเตือน

Collection Types

# Python 3.9+ — ใช้ built-in types ได้เลย
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
coordinates: tuple[float, float] = (13.7563, 100.5018)
unique_ids: set[int] = {1, 2, 3}

# Nested collections
matrix: list[list[int]] = [[1, 2], [3, 4]]
user_scores: dict[str, list[int]] = {
    "Alice": [95, 87, 92],
    "Bob": [78, 85, 90],
}

# Python 3.8 หรือต่ำกว่า — ต้อง import จาก typing
from typing import List, Dict, Tuple, Set
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 95}

Optional และ Union

# Optional — ค่าอาจเป็น None
from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)  # อาจ Return str หรือ None

# Union — ค่าเป็นได้หลาย Type
from typing import Union

def process(value: Union[int, str]) -> str:
    if isinstance(value, int):
        return str(value)
    return value.upper()

# Python 3.10+ — ใช้ | แทน Union ได้
def process(value: int | str) -> str:
    if isinstance(value, int):
        return str(value)
    return value.upper()

# Optional[X] = Union[X, None] = X | None (Python 3.10+)
def find_user(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

TypeAlias

# สร้าง Type alias เพื่อให้อ่านง่าย
from typing import TypeAlias

# Complex type → ใช้ Alias
UserId: TypeAlias = int
UserName: TypeAlias = str
UserMap: TypeAlias = dict[UserId, UserName]

def get_users() -> UserMap:
    return {1: "Alice", 2: "Bob"}

# JSON-like structure
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None

def parse_config(data: JSON) -> dict[str, str]:
    ...

# Vector / Coordinate
Vector2D: TypeAlias = tuple[float, float]
Vector3D: TypeAlias = tuple[float, float, float]

def distance(a: Vector2D, b: Vector2D) -> float:
    return ((a[0]-b[0])**2 + (a[1]-b[1])**2) ** 0.5

Callable

from typing import Callable

# Function ที่รับ Function เป็น Parameter
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

result = apply(lambda x, y: x + y, 3, 5)  # 8

# Callback pattern
def on_complete(callback: Callable[[str], None]) -> None:
    callback("Task done!")

# Function ที่ Return function
def multiplier(factor: int) -> Callable[[int], int]:
    def multiply(x: int) -> int:
        return x * factor
    return multiply

double = multiplier(2)
print(double(5))  # 10

Generics (TypeVar, Generic)

from typing import TypeVar, Generic

# TypeVar — สร้าง Generic type
T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

# ใช้ได้กับทุก Type
first([1, 2, 3])       # T = int
first(["a", "b", "c"]) # T = str

# Bounded TypeVar — จำกัด Type ที่ใช้ได้
from typing import Comparable
N = TypeVar("N", int, float)

def max_value(a: N, b: N) -> N:
    return a if a > b else b

# Generic Class
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

    def is_empty(self) -> bool:
        return len(self._items) == 0

# ใช้ Generic class
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop()  # mypy รู้ว่าเป็น int

str_stack: Stack[str] = Stack()
str_stack.push("hello")
word: str = str_stack.pop()  # mypy รู้ว่าเป็น str

Protocol (Structural Subtyping)

from typing import Protocol, runtime_checkable

# Protocol = Interface ของ Python
# ไม่ต้อง inherit — แค่มี method/attribute ตรง ก็ใช้ได้ (Duck Typing + Type Safety)

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render(shape: Drawable) -> None:
    shape.draw()

# Circle, Square ไม่ได้ inherit Drawable แต่ mypy ยอมรับ
# เพราะมี draw() method ตามที่ Protocol กำหนด
render(Circle())  # OK
render(Square())  # OK

# Runtime checkable Protocol
@runtime_checkable
class HasName(Protocol):
    name: str

class User:
    def __init__(self, name: str) -> None:
        self.name = name

u = User("Alice")
print(isinstance(u, HasName))  # True — ตรวจสอบ Runtime ได้

Dataclasses + Type Hints

from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class User:
    name: str
    email: str
    age: int
    is_active: bool = True
    created_at: datetime = field(default_factory=datetime.now)
    tags: list[str] = field(default_factory=list)

    def greet(self) -> str:
        return f"Hello, {self.name}!"

# Type hints + dataclass = สวยงาม ชัดเจน
user = User(name="Alice", email="alice@example.com", age=30)
print(user.greet())  # "Hello, Alice!"

# Frozen dataclass (Immutable)
@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def distance_to(self, other: "Point") -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0
# p1.x = 10  # Error! FrozenInstanceError (Immutable)

Pydantic Models

from pydantic import BaseModel, Field, EmailStr, validator
from datetime import datetime

class UserCreate(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)
    password: str = Field(..., min_length=8)
    tags: list[str] = []

    @validator("name")
    def name_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Name cannot be empty")
        return v.strip()

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    is_active: bool = True
    created_at: datetime

    class Config:
        from_attributes = True  # Pydantic v2

# Pydantic validate ที่ Runtime + mypy ตรวจสอบ Type ที่ Dev time
user = UserCreate(
    name="Alice",
    email="alice@example.com",
    age=30,
    password="secure123"
)
# ถ้า age="abc" → Pydantic raise ValidationError
# ถ้า age="abc" → mypy ก็แจ้งเตือน (int expected)

mypy — Static Type Checker

ติดตั้งและใช้งาน

# ติดตั้ง
pip install mypy

# ตรวจสอบไฟล์เดียว
mypy app.py

# ตรวจสอบทั้ง Project
mypy src/

# ตรวจสอบแบบ Strict (เข้มงวดสุด)
mypy --strict src/

# ตรวจสอบ + show error codes
mypy --show-error-codes src/

# ตรวจสอบเฉพาะ errors (ไม่แสดง warnings)
mypy --no-error-summary src/

mypy.ini Configuration

# mypy.ini หรือ setup.cfg หรือ pyproject.toml

# === mypy.ini ===
[mypy]
python_version = 3.12
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
show_error_codes = True

# Per-module config
[mypy-tests.*]
disallow_untyped_defs = False

[mypy-third_party_lib.*]
ignore_missing_imports = True

# === pyproject.toml ===
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

Strict Mode

# mypy --strict เปิด flags เหล่านี้ทั้งหมด:
# --disallow-untyped-defs        ทุก Function ต้องมี Type annotation
# --disallow-incomplete-defs     ถ้ามี Type บาง Parameter ต้องมีทุกตัว
# --disallow-untyped-calls       ห้ามเรียก Function ที่ไม่มี Type
# --disallow-any-generics        ห้ามใช้ list แบบไม่ระบุ Type (ต้อง list[int])
# --no-implicit-optional         ห้าม None เป็น default โดยไม่ระบุ Optional
# --warn-return-any              เตือนถ้า Return Any
# --warn-unreachable             เตือน Code ที่เข้าไม่ถึง

mypy ใน CI/CD

# GitHub Actions
name: Type Check
on: [push, pull_request]

jobs:
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install mypy
      - run: mypy --strict src/

# Pre-commit hook
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.9.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

pyright — ทางเลือกจาก mypy

# pyright เป็น Type checker จาก Microsoft (ใช้ใน VS Code / Pylance)
pip install pyright

# ตรวจสอบ
pyright src/

# pyright config ใน pyrightconfig.json
{
  "include": ["src"],
  "exclude": ["tests"],
  "pythonVersion": "3.12",
  "typeCheckingMode": "strict",
  "reportMissingImports": true,
  "reportMissingTypeStubs": false
}
Featuremypypyright
ภาษาที่เขียนPythonTypeScript (Node.js)
ความเร็วปานกลางเร็วมาก (10-100x)
IDE Integrationดี (Plugin)ดีมาก (VS Code Pylance built-in)
Strict modeมีมี (strict, basic, off)
Communityใหญ่ (Standard)โตเร็ว (Microsoft backed)
CI supportดีดี

Type Hints ใน FastAPI

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    quantity: int = 0

class ItemResponse(BaseModel):
    id: int
    name: str
    price: float
    quantity: int
    total: float

@app.post("/items/", response_model=ItemResponse)
async def create_item(item: Item) -> ItemResponse:
    # FastAPI ใช้ Type hints สำหรับ:
    # 1. Request validation (Pydantic)
    # 2. Response serialization
    # 3. OpenAPI docs generation
    return ItemResponse(
        id=1,
        name=item.name,
        price=item.price,
        quantity=item.quantity,
        total=item.price * item.quantity
    )

@app.get("/items/{item_id}")
async def get_item(item_id: int) -> ItemResponse:
    # item_id: int → FastAPI auto-convert path param to int
    if item_id <= 0:
        raise HTTPException(status_code=404, detail="Item not found")
    return ItemResponse(id=item_id, name="Sample", price=9.99, quantity=1, total=9.99)

Common Type Hint Patterns

# 1. Literal — ค่าที่เป็นได้เฉพาะที่กำหนด
from typing import Literal

def set_color(color: Literal["red", "green", "blue"]) -> None:
    print(f"Color: {color}")

set_color("red")    # OK
# set_color("pink")  # mypy Error!

# 2. Final — ค่าที่ห้ามเปลี่ยน (Constant)
from typing import Final

MAX_RETRY: Final[int] = 3
API_URL: Final = "https://api.example.com"
# MAX_RETRY = 5  # mypy Error! Cannot assign to final name

# 3. TypedDict — Dictionary ที่มีโครงสร้างชัดเจน
from typing import TypedDict

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

user: UserDict = {"name": "Alice", "age": 30, "email": "alice@example.com"}
# user["name"] → mypy รู้ว่าเป็น str
# user["score"] → mypy Error! key ไม่มีใน UserDict

# 4. Annotated — เพิ่มข้อมูลเพิ่มเติม (Metadata)
from typing import Annotated

UserId = Annotated[int, "User ID must be positive"]
Percentage = Annotated[float, "Value between 0.0 and 1.0"]

def get_user(user_id: UserId) -> str:
    return f"User {user_id}"

# 5. Type Guard (Python 3.10+)
from typing import TypeGuard

def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process(data: list[object]) -> None:
    if is_str_list(data):
        # mypy รู้ว่า data เป็น list[str] ใน block นี้
        for item in data:
            print(item.upper())  # OK — item เป็น str

Gradual Typing — เพิ่ม Types ให้ Legacy Code

ไม่จำเป็นต้องเพิ่ม Type hints ทุกที่ในคราวเดียว ค่อยๆ เพิ่มได้:

  1. เริ่มจาก Function signatures: เพิ่ม Type ให้ Parameter และ Return type ก่อน
  2. Public API ก่อน: Function/Class ที่คนอื่นใช้ → เพิ่ม Type ก่อน
  3. ไฟล์ใหม่: ไฟล์ใหม่ทุกไฟล์ต้องมี Type hints ตั้งแต่แรก
  4. ค่อยๆ เพิ่ม Strict level: เริ่มจาก mypy src/mypy --disallow-untyped-defs src/mypy --strict src/
  5. ใช้ # type: ignore ชั่วคราว: สำหรับ Code ที่ยังไม่มีเวลาแก้
# ขั้นตอนที่ 1: เพิ่ม py.typed marker
# สร้างไฟล์ว่าง src/py.typed

# ขั้นตอนที่ 2: เริ่มจาก basic mypy config
# mypy.ini
[mypy]
python_version = 3.12
warn_return_any = True
# ยังไม่เปิด strict

# ขั้นตอนที่ 3: เพิ่ม Type ทีละไฟล์
# ใช้ monkeytype หรือ pytype generate type stubs จาก runtime
pip install monkeytype
monkeytype run my_script.py
monkeytype stub my_module  # ดู Type ที่ detect ได้
monkeytype apply my_module  # Apply type hints ลง Code

Runtime Type Checking

# beartype — Runtime type checking decorator
pip install beartype

from beartype import beartype

@beartype
def add(a: int, b: int) -> int:
    return a + b

add(1, 2)     # OK
# add("1", 2)  # Runtime Error! BeartypeCallHintParamViolation

# typeguard — Runtime type checking
pip install typeguard

from typeguard import typechecked

@typechecked
def greet(name: str) -> str:
    return f"Hello, {name}"

greet("Alice")  # OK
# greet(123)    # Runtime Error! TypeCheckError

Best Practices

  1. ใช้ Python 3.10+ syntax: int | str แทน Union[int, str], list[int] แทน List[int]
  2. ทุก Function ต้องมี Return type: แม้แต่ -> None
  3. ใช้ from __future__ import annotations: (Python 3.7-3.9) เพื่อใช้ syntax ใหม่
  4. อย่าใช้ Any ถ้าไม่จำเป็น: Any = ปิด Type checking
  5. ใช้ Protocol แทน Abstract Base Class: Pythonic กว่า รองรับ Duck Typing
  6. ใช้ TypedDict สำหรับ Dict ที่มี Structure: ดีกว่า dict[str, Any]
  7. เปิด mypy ใน CI: ป้องกัน Type error เข้า Production
  8. ใช้ Pydantic สำหรับ Data validation: Type hints + Runtime validation ในตัว

Python Type Hints เปลี่ยนวิธีเขียน Python ให้ปลอดภัยและมั่นใจมากขึ้น ไม่ต้องรอ Runtime ถึงจะเจอ Bug ใช้ mypy + VS Code + Type Hints แล้วคุณจะรู้สึกเหมือนเขียน TypeScript/Go — จับ Bug ได้ตั้งแต่ตอนเขียน ไม่ใช่ตอน Deploy ขึ้น Production


Back to Blog | iCafe Forex | SiamLanCard | Siam2R