Python 型別提示 (Type Hints) 與 Typing

Python 是一種動態型別 (Dynamically Typed) 的語言,這意味著變數的型別是在執行時期 (Runtime) 決定的,我們不需要(也無法強制)在宣告變數時指定型別。

然而,隨著專案規模變大,缺乏型別資訊會讓程式碼難以閱讀和維護。為了解決這個問題,Python 3.5 引入了型別提示 (Type Hints)

為什麼要使用型別提示?

  1. 增加可讀性:明確指出函式需要什麼參數、會回傳什麼,讓其他開發者(包括未來的自己)更容易看懂程式碼。
  2. IDE 支援:現代的編輯器 (如 VS Code, PyCharm) 可以利用型別資訊提供更好的自動完成 (Auto-completion) 和錯誤提示。
  3. 減少 Bug:搭配靜態型別檢查工具 (如 mypy),可以在執行前就發現型別錯誤。
Python 的型別提示不會在執行時期強制檢查型別。也就是說,即使你傳入了錯誤的型別,Python 直譯器通常不會報錯(除非該型別導致了後續的操作失敗)。型別提示主要是給開發者和工具看的。

基本語法

變數型別註釋

使用 變數: 型別 的語法:

name: str = "Alice"
age: int = 30
is_active: bool = True
height: float = 1.75

也可以只宣告不賦值(但通常不建議這樣做,除非在類別屬性中):

score: int
# ... 稍後再賦直
score = 100

函式型別註釋

我們可以標註 參數型別回傳值型別 (使用 ->):

def greeting(name: str) -> str:
    return "Hello, " + name

def add(x: int, y: int) -> int:
    return x + y

容器型別 (Collection Types)

這部分在 Python 3.9 前後有一些變化。

Python 3.9+ (推薦)

從 Python 3.9 開始,我們可以直接使用內建的容器類別 (如 list, dict, set) 來作為型別提示:

# 一個包含整數的列表
scores: list[int] = [10, 20, 30]

# 一個 key 為字串,value 為整數的字典
user_ages: dict[str, int] = {
    "Alice": 30,
    "Bob": 25
}

# 一個包含字串的集合
tags: set[str] = {"python", "coding"}

# 一個包含特定三個元素的元組
point: tuple[int, int, int] = (1, 2, 3)

Python 3.8 及更早版本

在舊版 Python 中,你需要從 typing 模組引入對應的大寫類別:

from typing import List, Dict, Set, Tuple

scores: List[int] = [10, 20, 30]
user_data: Dict[str, str] = {"name": "Alice"}

typing 模組的特殊型別

對於更複雜的型別需求,我們需要使用 typing 模組。

Any (任意型別)

Any 表示該變數可以是任何型別。這等於是告訴檢查工具:「這裡不要檢查型別」。

from typing import Any

def print_anything(value: Any) -> None:
    print(value)

print_anything(123)
print_anything("Hello")

Union (聯合型別)

當一個變數可能是多種型別之一時使用。

Python 3.10+ (推薦寫法,使用 |):

def process_id(user_name: str, user_id: int | str):
    # user_id 可以是整數或字串
    print(f"User: {user_name}, ID: {user_id}")

舊版寫法 (使用 Union):

from typing import Union

def process_id(user_id: Union[int, str]):
    pass

Optional (可選型別)

Optional[T] 等同於 Union[T, None]T | None,表示該值可能是型別 T,或是 None。這常用於有預設值的參數。

# Python 3.10+ 推薦寫法
def find_user(user_id: int) -> dict | None:
    if user_id == 1:
        return {"name": "Admin"}
    return None

# 舊版寫法
from typing import Optional

def find_user_legacy(user_id: int) -> Optional[dict]:
    ...

Literal (字面值型別)

有時候我們希望變數不只是某種型別,還必須是特定的幾個值

from typing import Literal

def set_status(status: Literal["pending", "approved", "rejected"]):
    print(f"Status set to: {status}")

set_status("approved")  # OK
# set_status("deleted")  # 靜態檢查工具會報錯

TypeAlias (型別別名)

如果某個型別組合很長且多次使用,我們可以使用 TypeAlias 定義一個別名,增加可讀性。

from typing import TypeAlias

# 定義一個複雜的型別別名
Coordinates: TypeAlias = list[tuple[float, float]]

def process_path(path: Coordinates):
    for x, y in path:
        print(f"Moving to {x}, {y}")

Annotated (附加資訊型別)

Annotated 是 Python 3.9 引入的一個非常強大的功能。它允許你在型別提示中附加中繼資料 (Metadata)

這個語法本身不會改變型別檢查的結果,但許多框架(如 FastAPIPydantic)會利用這些中繼資料來進行資料驗證或依賴注入。

from typing import Annotated

# 基本語法:Annotated[原始型別, 中繼資料1, 中繼資料2, ...]
# 以下範例表示 userId 是 int,且附加了說明文字
UserId: TypeAlias = Annotated[int, "這是使用者的唯一識別碼"]

def get_user(uid: UserId):
    return f"User {uid}"

在 FastAPI 中的應用:

from fastapi import Query
from typing import Annotated

# 表示 q 是一個字串,且長度必須在 3 到 10 之間
def search(q: Annotated[str | None, Query(min_length=3, max_length=10)] = None):
    return q

TypedDict (強型別字典)

普通的 dict[str, Any] 無法限制字典裡有哪些 Key。TypedDict 可以讓你定義字典的固定結構。

from typing import TypedDict

class UserProfile(TypedDict):
    name: str
    age: int
    email: str | None

# 建立符合結構的字典
user: UserProfile = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com"
}

Protocol (結構化子型別)

Protocol 類似於 Java 或 Go 中的 Interface(介面)。它定義了一種契約,只要某個類別實踐了 Protocol 定義的方法,它就被視為該型別,而不需要顯式繼承。

from typing import Protocol

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

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

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

def render(shape: Drawable):
    shape.draw()

render(Circle())  # OK,因為 Circle 有 draw 方法
render(Square())  # OK

Self (自身型別)

在類別方法中,如果回傳值是「類別自己」,Python 3.11 引入了 Self 讓定義更簡單。

from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self

builder = Builder().set_name("MyBuilder")

Callable (可呼叫物件)

當參數是需要傳入一個函式時使用。 語法:Callable[[參數型別1, 參數型別2], 回傳型別]

from typing import Callable

def apply_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
    return func(x, y)

def multiply(a: int, b: int) -> int:
    return a * b

print(apply_func(5, 3, multiply))  # 15

Final (常數)

用來標示變數不應該被重新賦值(類似其他語言的 const)。

from typing import Final

PI: Final[float] = 3.14159

# PI = 3.14  # 靜態檢查工具會報錯

泛型 (Generics) 與 TypeVar

有時候我們希望函式的輸入型別和輸出型別是有關聯的,這時可以使用 TypeVar

from typing import TypeVar

T = TypeVar('T')

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

# 如果傳入 list[int],回傳就是 int
print(get_first_element([1, 2, 3]))

# 如果傳入 list[str],回傳就是 str
print(get_first_element(["a", "b", "c"]))

靜態型別檢查工具 (mypy)

正如前面提到,Python 執行時不會檢查這些提示。要真正發揮型別提示的威力,你需要使用靜態檢查工具。最常用的是 mypy

安裝:

pip install mypy

執行檢查:

mypy script.py

如果有型別不符的地方,mypy 就會報錯,幫助你在寫程式的階段就抓出 Bug。