---
# System prepended metadata

title: typing
tags: [Python's Flying Circus, Type]

---

# typing

[![RogelioKG/typing](https://img.shields.io/badge/Sync%20with%20HackMD-grey?logo=markdown)](https://hackmd.io/@RogelioKG/typing)

## References
+ 🔗 [**好豪 - Python Type Hints 教學：我犯過的 3 個菜鳥錯誤**](https://haosquare.com/python-type-hints-3-beginner-mistakes/)
+ 🔗 [**MyApollo - Python 的 typing.Protocol 怎麼使用？**](https://myapollo.com.tw/blog/python-typing-protocol/)
+ 📜 [**Python Typing Documentation**](https://typing.python.org/en/latest/index.html)

## Type Hinting

| 📗 <span class="tip">TIP</span> |
| :-------- |
| `type()` 類別比對是嚴格相等，`isinstance()` 的類別比對有考慮到繼承關係 |

| 📗 <span class="tip">TIP</span> |
| :-------- |
| 若標註型態是 `float`，接受型態是 `int`，mypy 還是會讓你過的。<br />因為 `int` ⊂ `float` ⊂ `complex`，所以不用刻意去寫 <code>int &#124; float</code>。<br />詳見 [PEP-3141] – A Type Hierarchy for Numbers。 |

### Basic
+ 範例
  ```py
  x: float
  x: list[int] = [1]
  x: set[int] = {6, 7}
  x: dict[str, float] = {"field": 2.0}
  x: tuple[int, str, float] = (3, "yes", 7.5)
  x: tuple[int, ...] = (1, 2, 3)
  ```
  ```py
  x: Any # 任意型態
  x: Literal[4] # 字面量 4
  x: Callable[[int, bytes], str] # def x(a: int, b: bytes) -> str: ...

  x: list[int | str] = [3, 5, "test", "fun"]
  x: list[Union[int, str]] = [3, 5, "test", "fun"]

  x: str | None
  x: Optional[str] = "something" if some_condition() else None
  ```

### args & kwargs
+ 說明
  + type hinting 是給定 <mark>value 的型態</mark>
+ 範例
  ```py
  def foo(*args: str, **kwargs: int):
      print(f"args: {args}")
      print(f"kwargs: {kwargs}")

  if __name__ == "__main__":
      foo("a", "b", "c", x=1, y=2, z=3)
  ```

### Variadic Generics
+ 說明
  + [PEP-646] (2020/09/16)
+ 新式寫法：`*` (Python 3.12)
  > 舊式寫法：[Unpack](#Unpack：tuple-引數型別解包)
  ```py
  def prepend[T, *Ts](element: T, collection: tuple[*Ts]) -> tuple[T, *Ts]:
      return (element, *collection)

  z = prepend(element=0, collection=(True, "a"))
  # z: tuple[int, bool, str]
  ```
+ ⚠️ [嚴禁 Type Parameter 中出現多個 Variadic Generics](https://peps.python.org/pep-0646/#multiple-type-variable-tuples-not-allowed)
  > 因為無法去確認其中每個元素具體屬於哪個 Variadic Generic
  ```py
  class Array[*Ts1, *Ts2]:
      def test(self, n: tuple[*Ts1]):
          return n

  x: Array[int, str, bool] = Array()
  # 那這樣 Ts1 是 [int]、[int, str] 還是 [int, str, bool]？
  x.test(2)
  x.test(2, "3", True)
  ```


### Type Parameter Syntax
+ 說明
  + [PEP-695] (2022/06/15)
+ 新式寫法：`[T]` (Python 3.12)
  > 舊式寫法：[TypeVar](#TypeVar：型別參數)、[Generic](#Generic：泛型)
  ```py
  def func[T](a: T) -> T:
      return a
  ```
  ```py
  class Stack[T]:
      def __init__(self) -> None:
          self._items: list[T] = []
  ```
  ```py
  class Matrix[A, B]:
      def multiply[C](self, other: "Matrix[B, C]") -> "Matrix[A, C]": ...

  x: Matrix[Literal[30], Literal[20]] = ...
  y: Matrix[Literal[20], Literal[50]] = ...
  
  z = x.multiply(y)
  # z: Matrix[Literal[30], Literal[50]]
  ```
+ 新式寫法：`**` (Python 3.12)
  > 舊式寫法：[ParamSpec](#ParamSpec：函式簽名型別)
  ```py
  from typing import Callable

  def to_str[**P, R](func: Callable[P, R]) -> Callable[P, str]:
      def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
          result = func(*args, **kwargs)
          return str(result)
      return wrapper

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

  a = multiply(3, 2)
  # a 可被正確的判定成 str 型別 (以往會誤判成 int)
  ```

## Utility Types

### `TypeAlias`：型別別名
+ 範例
  ```py
  Vector: TypeAlias = list[float] # 顯式宣稱其為別名，不是一個變數
  ```

### `TypedDict`：型別字典
+ 範例
  ```py
  class Point2D(TypedDict): # total=False：全體屬性 Partial
      x: int
      y: int
      label: str # NotRequired[str]：個別屬性 Partial


  a: Point2D = {"x": 1, "y": 2, "label": "good"}  # OK
  b: Point2D = {"z": 3, "label": "bad"}  # Fails type check

  assert Point2D(x=1, y=2, label="first") == dict(x=1, y=2, label="first")
  ```



### `NoReturn`：永不回傳
+ 說明
  + 只要有機會正常回傳，或隱式回傳 None，都不算！
+ 範例
  ```py
  # 若你註釋 NoReturn，調用此類函數後的程式碼，是無法存取的 (unreachable)
  # IDE 很貼心，會將其後程式碼黯淡化
  def stop() -> NoReturn:
      raise RuntimeError("no way")


  if __name__ == "__main__":
      stop()
      a = 5
  ```

### `Annotated`：註釋
+ 說明
  + 型別標註的額外資訊
  + 可被 `inspect.signature()` 提煉出來
  + 常被 static type checker、第三方函式庫等利用
    + [Pydantic - Validator](https://docs.pydantic.dev/latest/concepts/validators/#__tabbed_1_1)
    + [FastAPI - Depends](https://fastapi.tiangolo.com/tutorial/dependencies/#import-depends)
+ 範例
  ```py
  from typing import Annotated


  def hello(name: Annotated[str, "first letter is capital"]):
      print(f"hello {name}!")
  ```
  > 手刻驗證器 (向 Pydantic 致敬一波)
  ```py
  import inspect
  from functools import wraps
  from typing import Annotated, get_args

  # validator
  def min_length_3(value: str) -> str:
      if len(value) < 3:
          raise ValueError(f"'{value}' is too short, must be at least 3 characters.")
      return value

  def is_adult(value: int) -> int:
      if value < 18:
          raise ValueError(f"{value} is not an adult age (must be ≥ 18).")
      return value

  def is_email(value: str) -> str:
      if "@" not in value:
          raise ValueError(f"'{value}' is not a valid email.")
      return value

  # decorator
  def injectable(func):
      @wraps(func)
      def wrapper(*args, **kwargs):
          # ⚠️ 仔細看這裡是怎麼取得註釋內的 validator
          sig = inspect.signature(func)
          for arg, param in zip(args, sig.parameters.values()):
              annotation = param.annotation
              validator = get_args(annotation)[1]
              validator(arg)
          return func(*args, **kwargs)
      return wrapper

  @injectable
  def create_user(
      username: Annotated[str, min_length_3],
      age: Annotated[int, is_adult],
      email: Annotated[str, is_email]
  ):
      return {
          "username": username,
          "age": age,
          "email": email,
      }

  print(create_user("Tommy", 21, "tommy@example.com"))    # ✅
  print(create_user("Al", 22, "al@example.com"))          # ❌ username 太短
  print(create_user("Alice", 16, "alice@example.com"))    # ❌ 未成年
  print(create_user("Bob", 30, "bobexample.com"))         # ❌ email 無效
  ```

### `TypeVar`：型別參數
+ 範例
  ```py
  T = TypeVar("T")
  # 型別 T 可為任意型別

  U = TypeVar("U", str, bytes)
  # 型別 U 須為 str 或 bytes

  V = TypeVar("V", bound=str)
  # 型別 V 須為 str 或其子類別 (Java 觀點：<? extends V>)

  List = TypeVar("List", covariant=True)
  # 型別 List 具有協變性
      # 已知 Cat 類別是 Animal 類別的子類別，
      # 若 List<Cat> 可視為 List<Animal> 的子型別，
      # 則 List<T> 具有協變性 (covariant)。

  Dict = TypeVar("Dict", contravariant=True)
  # 型別 Dict 具有逆變性
      # 已知 Cat 類別是 Animal 類別的子類別，
      # 若 List<Animal> 可視為 List<Cat> 的子型別，
      # 則 List<T> 具有逆變性 (contravariant)。
  # 在沒有指定變異性的情況下，預設具有不變性
      # 已知 Cat 類別是 Animal 類別的子類別，
      # 若 List<Cat> 與 List<Animal> 沒有子型別關係，
      # 則 List<T> 具有不變性 (invariant)。



  def repeat(x: T, n: int) -> list[T]:
      "Return a list containing n references to x."
      return [x] * n


  def longest(x: U, y: U) -> U:
      "Return the longest of two strings."
      return x if len(x) >= len(y) else y


  if __name__ == "__main__":
      # TypeVar 屬性
      print(T.__name__)
      # T
      print(U.__constraints__)
      # (<class 'str'>, <class 'bytes'>)
      print(U.__covariant__)
      # False
      print(List.__covariant__)
      # True
      print(Dict.__contravariant__)
      # True

      # 正確範例
      print(repeat(10, 6))
      print(repeat('a', 6))
      print(longest("1234567", "123"))
      print(longest(b"1234567", b"123"))
      # 錯誤範例
      # (這裡型別是 list[int] 就錯了，雖然可正常運行，但 mypy 等 static type checker 會檢查出來)
      print(longest([1, 2, 3, 4, 5, 6, 7], [1, 2, 3]))
  ```

### `Generic`：泛型
+ 範例
  ```py
  # 說明：IDE 會補上詳細的提示，對 API 使用者友好

  KT = TypeVar("KT")  # key type
  VT = TypeVar("VT")  # value type


  # 繼承泛型
  class Map(Generic[KT, VT]):
      def __init__(self, key_arr: Iterable[KT], value_arr: Iterable[VT]):
          assert len(key_arr) == len(value_arr)
          self.__keys = key_arr
          self.__values = value_arr

      def get(self, key: KT) -> VT:
          for i, k in enumerate(self.__keys):
              if k == key:
                  break
          return self.__values[i]


  if __name__ == "__main__":
      num_arr = [1, 2, 3, 4, 5]
      char_arr = ["a", "b", "c", "d", "e"]
      m = Map(num_arr, char_arr)
      print(m.get(1))

  # 完全不給型別提示
  # (method) def get(key: Any) -> Any

  # 未補上泛型的 get 方法註釋
  # (method) def get(key: KT@get) -> VT@get

  # 補上泛型的 get 方法註釋
  # (method) def get(key: int) -> str
  ```


### `Unpack`：tuple 引數型別解包
+ 範例
  ```py
  from typing import TypedDict, TypeVar, TypeVarTuple, Unpack

  T = TypeVar("T")
  Ts = TypeVarTuple("Ts")

  def prepend(element: T, collection: tuple[Unpack[Ts]]) -> tuple[T, Unpack[Ts]]:
      return (element, *collection)

  z = prepend(element=0, collection=(True, "a"))
  # z: tuple[int, bool, str]
  ```
  > 對於 `TypedDict` 提供的 IDE 提示特別好用
  ```py
  class UserInfo(TypedDict):
      name: str
      age: int

  def print_user_info(**kwargs: Unpack[UserInfo]) -> None:
      print(f"Name: {kwargs['name']}, Age: {kwargs['age']}")

  # 提示：(*, name: str, age: int) -> None
  print_user_info(name="Alice", age=30)
  ```

### `ParamSpec`：函式簽名型別
+ 說明
  + 為了避免 closure 屏蔽掉回傳型別
+ 範例
  ```py
  from typing import Callable, TypeVar, ParamSpec

  P = ParamSpec('P')
  R = TypeVar('R')

  def to_str(func: Callable[P, R]) -> Callable[P, str]:
      def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
          result = func(*args, **kwargs)
          return str(result)
      return wrapper

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

  a = multiply(3, 2)
  # a 可被正確的判定成 str 型別 (以往會誤判成 int)
  ```

### `Self`：實例自身
+ 說明
  + [PEP-673]
+ 範例
  > `set_scale` 回傳的是 `Shape`，然而 `Shape` 並沒有 `set_radius` 方法。\
  > static type checker 會報錯。
  ```py
  from __future__ import annotations


  class Shape:
      def set_scale(self, scale: float) -> Shape: # ❓
          self.scale = scale
          return self


  class Circle(Shape):
      def set_radius(self, r: float) -> Circle:
          self.radius = r
          return self


  Circle().set_scale(0.5).set_radius(2.7)
  ```
  > 解法：自己設 `TypeVar`，有點麻煩
  ```py

  from __future__ import annotations

  from typing import TypeVar

  TShape = TypeVar("TShape", bound="Shape")


  class Shape:
      def set_scale(self: TShape, scale: float) -> TShape: # 🤔
          self.scale = scale
          return self


  class Circle(Shape):
      def set_radius(self, radius: float) -> Circle:
          self.radius = radius
          return self


  Circle().set_scale(0.5).set_radius(2.7)  # => Circle
  ```
  > 3.11 後有個更簡單的寫法 `Self`
  ```py
  class Shape:
      def set_scale(self, scale: float) -> Self: # ✅
          self.scale = scale
          return self


  class Circle(Shape):
      def set_radius(self, radius: float) -> Circle:
          self.radius = radius
          return self


  Circle().set_scale(0.5).set_radius(2.7)
  ```

### `Protocol`：協議
+ 說明
  > 協議是 Duck Typing 的體現。\
  > 它允許你在不使用繼承的情況下達成多型，\
  > 這<mark>可被 static type checker (如 mypy) 檢查出</mark>。

  > 一言以蔽之：\
  > 繼承是<mark>名義性的 (nominal)</mark>、<mark>侵入性的</mark>，\
  > 協議是<mark>結構性的 (structural)</mark>、<mark>非侵入性的</mark>。
  > 協議就像 `interface`。

  > 這其實在某些情況下相當好用，\
  > 假如你使用到某三方庫的某類別 `EarBuds`，\
  > 你的代碼仰賴其中的 `connect`、`disconnect` 方法。\
  > 你想要將其抽象化，變成一個允許連線或斷線的介面 `Device`，\
  > 好方便你未來去抽換你自己實作的各種 `Device`。\
  > 問題是，你無法修改這個 `EarBuds` 類別 (人家是第三方庫)，\
  > 你就不能讓它繼承你定義的 `Device` 介面，\
  > 此時傳統的繼承就派不上用場了，這正是協議發揮威力的時刻。

+ 範例
  ```py
  from typing import Protocol

  class Device(Protocol):
      def connect(self) -> None:
          ...
      def disconnect(self) -> None:
          ...

  class SmartSpeaker:
      def connect(self) -> None:
          print("connect device")
      def disconnect(self) -> None:
          print("disconnect device")

  def connect_device(device: Device) -> None:
      device.connect()


  device = SmartSpeaker()
  connect_device(device)
  ```




### `SupportsX`：魔術方法協議
+ 說明
  + 表示某個擁有 `__index__` 魔術方法的類別
+ 範例
  ```py
  # typing 內部實作
  @runtime_checkable
  class SupportsIndex(Protocol):

      __slots__ = ()

      @abstractmethod
      def __index__(self) -> int:
          pass
  ```




### `TypeGuard`：呃...
+ 說明
  + [PEP-647] (2020/10/07)
  + <mark>並不是真正的型別守衛，其會在 Union 型別分支的情況判斷錯誤</mark>
+ 範例
  ```py
  from typing import Literal, TypedDict, TypeGuard, assert_type


  class Cat(TypedDict):
      kind: Literal["cat"]
      meow: str


  class Dog(TypedDict):
      kind: Literal["dog"]
      bark: str


  def is_cat(val: Cat | Dog) -> TypeGuard[Cat]:
      return val["kind"] == "cat"


  def make_sound(animal: Cat | Dog):
      if is_cat(animal):
          assert_type(animal, Cat)
          # ✅ animal 被縮窄為 Cat
      else:
          assert_type(animal, Dog)
          # ❓ error: Expression is of type "Cat | Dog", not "Dog"
  ```

### `TypeIs`：型別守衛
+ 說明
  + 這才是真正的型別守衛！
+ 範例
  ```py
  from typing import Literal, TypedDict, TypeIs, assert_type


  class Cat(TypedDict):
      kind: Literal["cat"]
      meow: str


  class Dog(TypedDict):
      kind: Literal["dog"]
      bark: str


  def is_cat(val: Cat | Dog) -> TypeIs[Cat]:
      return val["kind"] == "cat"


  def make_sound(animal: Cat | Dog):
      if is_cat(animal):
          assert_type(animal, Cat)
          # ✅ animal 被縮窄為 Cat
      else:
          assert_type(animal, Dog)
          # ✅ animal 被縮窄為 Dog

  ```
  ```ts
  // TypeScript 的 TypeGuard 範例
  type Dog = { kind: "dog"; bark(): void };
  type Cat = { kind: "cat"; meow(): void };
  type Animal = Dog | Cat;

  function isCat(animal: Animal): a is Cat {
    return animal.kind === "cat";
  }

  function makeSound(animal: Animal) {
    if (isCat(animal)) {
      animal.meow(); // ✅ animal 被縮窄為 Cat
    } else {
      animal.bark(); // ✅ animal 被縮窄為 Dog
    }
  }
  ```

## Functions

### `cast`
+ 說明
  + 轉型 (為了讓 type checker 閉嘴 ==)
+ 範例
  ```py
  metadata = cast(dict[str, Any], metadata)
  ```

### `get_origin`
+ 範例
  ```py
  print(get_origin(list[int])) # <class 'list'>
  ```

### `get_args`
+ 範例
  ```py
  print(get_args(list[int])) # (<class 'int'>,)
  ```


### `final`：無法繼承、無法覆寫
+ 說明
  + 被裝飾的類別無法被繼承 (inherit)
  + 被裝飾的方法無法覆寫 (override)
+ 範例
  ```py
  @final
  class Car():
      @final
      def meow():
          pass
  ```

### `overload`：函數多載
+ 範例
  ```py
  @overload
  def utf8(value: bytes) -> bytes: ...
  @overload
  def utf8(value: str) -> bytes: ...
  # 實作
  def utf8(value: str | bytes) -> str | bytes:
      pass
  ```
+ 用途
  > 可看到實作處的型別是混雜的，\
  > 若你想為不同簽名的函式做出不同實作，你要自己比對型別 (`isinstance`)。\
  > 假如<mark>函式因不同參數簽名回傳不同型態</mark>，\
  > 使用 `@overload` 能有效地告訴 static type checker，\
  > 當傳入某某參數時，回傳型態是什麼？
  ```py
  @overload
  def enable_repr[T](
      cls: None,
      *,
      sensitive: set[str] | None = None,
  ) -> Callable[[type[T]], type[T]]: ...


  @overload
  def enable_repr[T](
      cls: type[T],
      *,
      sensitive: set[str] | None = None,
  ) -> type[T]: ...


  def enable_repr[T](
      cls: type[T] | None = None,
      *,
      sensitive: set[str] | None = None,
  ) -> Callable[[type[T]], type[T]] | type[T]:
      """自動為每個 SQLAlchemy Model 提供 `__repr__` 方法，並屏蔽敏感欄位

      Example
      -------
      >>> @enable_repr(sensitive={"password", "email"})
      ... class User(Base):
      ...     __tablename__ = "User"
      ...     id = mapped_column(Integer, primary_key=True)
      ...     username = mapped_column(String(50))
      ...     email = mapped_column(String(100))
      ...     password = mapped_column(String(100))
      >>> user = User(id=1, username="john", email="john@example.com", password="123")
      >>> print(user)
      User(id=1, username='john', email=***, password=***)
      """

      if sensitive is None:
          sensitive = set()

      def wrapper(cls_: type[T]) -> type[T]:
          setattr(cls_, "_sensitive", sensitive)  # noqa: B010

          def __repr__(self) -> str:
              columns = class_mapper(self.__class__).columns.keys()
              repr_dict = {}
              for col in columns:
                  value = getattr(self, col)
                  if col in self.__class__._sensitive:
                      repr_dict[col] = "***"
                  else:
                      repr_dict[col] = repr(value)
              repr_data = ", ".join(f"{k}={v}" for k, v in repr_dict.items())
              return f"{self.__class__.__name__}({repr_data})"

          cls_.__repr__ = __repr__
          return cls_

      return wrapper if cls is None else wrapper(cls)
  ```

## Others


### `from __future__ import annotation`
+ 說明
  + 未加上前：parser 會試圖找到 type hinting 中，各個型別的定義
    1. 若 type hinting 中出現【未定義的引用】，會導致 `NameError`
    2. 若 type hinting 中出現【無法辨識的語法】，會導致 `SyntaxError`
  + 加上後：<mark>將所有 type hinting 通通當作普通字串看待</mark>
    1. type hinting 將允許前向聲明、循環引用
    2. type hinting 將允許新式 typing hinting 語法，又同時支援舊版 Python
        > 寫支援多版本套件時，非常實用
+ 範例
  ```py
  from __future__ import annotations # 一定要寫在第一行


  class Node:
      def __init__(self, next_node: Node | None = None):
          self.next = next_node
  ```
  ```py
  # 可以不加，那你的未定義引用就要自己寫成字串

  class Node:
      def __init__(self, next_node: "Node" | None = None):
          self.next = next_node
  ```

### covariant, invariant, contravariant
+ 範例
  ```py
  from typing import Generic, TypeVar, Iterable, Iterator

  # 若容器型態不具協變性 (把 covariant=True 去掉)，
  # 意即只具備不變性的話 (看 dump_employees 函數)...
  T = TypeVar("T", covariant=True)


  class ImmutableList(Generic[T]):
      def __init__(self, items: Iterable[T]) -> None:
          self._items: list[T] = list(items)

      def __iter__(self) -> Iterator[T]:
          return iter(self._items)

      def __len__(self) -> int:
          return len(self._items)

      def __getitem__(self, index: int) -> T:
          return self._items[index]

      def __repr__(self) -> str:
          return f"ImmutableList({self._items})"


  class Employee:
      def __init__(self, id: int) -> None:
          self.id = id

      def __repr__(self) -> str:
          return f"Employee(id:{self.id})"


  class Manager(Employee):
      def __init__(self, id: int) -> None:
          super().__init__(id)

      def __repr__(self) -> str:
          return f"Manager(id:{self.id})"


  # 使用 mypy 等 static type checker 時
  # 若此處傳入 ImmutableList[Manager] 會出錯
  # 因為 ImmutableList[Manager] is not a ImmutableList[Employee]
  def dump_employees(employees: ImmutableList[Employee]) -> None:
      for employee in employees:
          print(employee)


  if __name__ == "__main__":
      employees = ImmutableList([Employee(i) for i in range(1, 6)])
      dump_employees(employees)
      managers = ImmutableList([Manager(i) for i in range(6, 11)])
      dump_employees(managers)

  ```


[PEP-646]: https://peps.python.org/pep-0646/
[PEP-647]: https://peps.python.org/pep-0647/
[PEP-673]: https://peps.python.org/pep-0673/
[PEP-695]: https://peps.python.org/pep-0695/
[PEP-3141]: https://peps.python.org/pep-3141/