제네릭은 타입을 매개변수로 받고, Protocol은 필요한 속성과 메서드의 모양을 선언한다.

from typing import Protocol, TypeVar

class Chooser(Protocol):
    def choose(self, board: str) -> str: ...

T = TypeVar("T")

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

`first([1, 2])`의 반환은 int로, 문자열 목록이면 str로 추론된다. 입력과 출력의 관계가 없는데 무조건 TypeVar를 쓰면 복잡성만 늘어난다.

공변성과 반공변성은 타입 계층이 제네릭 바깥으로 어떻게 이어지는지 정한다. 값을 생산만 하는 읽기 전용 컨테이너는 공변일 수 있고, 소비만 하는 콜백 인자는 반공변일 수 있다. 읽고 쓰는 변경 가능 컨테이너는 보통 불변이다.

런타임 `isinstance`에도 Protocol을 쓰려면 `@runtime_checkable`이 필요하지만, 메서드 시그니처까지 검사하지는 않는다.

Python 19. Protocol과 제네릭의 핵심 흐름도
그림. Protocol은 명시적 상속 없이 필요한 객체 모양을 표현한다.