📌 Introduction to knowledge points

Many Python developers’ understanding of type annotations remains at name: str = "hello" and def add(a: int, b: int) -> int. But the typing module Generics, covariance and contravariance, Protocol (structural subtype) These high-level features allow you to write code that is both flexible and can pass strict type checking. Today we focus on three high-frequency scenarios:Generic functionProtocol (formalization of duck typing),and TypedDict(structured dictionary)


🔧 Core code

1. TypeVar:Write a function that "accepts multiple types"

from typing import TypeVar, List

T = TypeVar("T")  # 类型变量,调用时自动推导

def first(items: List[T]) -> T | None:
    """返回列表的第一个元素,类型与列表元素类型一致"""
    return items[0] if items else None

# 使用
x: int | None = first([1, 2, 3])        # 推导出 T = int
y: str | None = first(["a", "b"])       # 推导出 T = str
z: float | None = first([])             # 推导出 T = float,返回 None

boundary constraints: Limit the scope of generic parameters

from typing import TypeVar

# 只有 int 和 float 可以传入
Number = TypeVar("Number", int, float)

def double(n: Number) -> Number:
    return n * 2

double(5)      # ✅ 10
double(3.14)   # ✅ 6.28
double("hi")   # ❌ 类型检查报错

2. Protocol: Formal declaration of duck typing (structural subtype)

Traditional abstraction (ABC/inheritance) requires explicit inheritance, whereas Protocol Only require objects to have specific methods/properties - this is the true spirit of Python.

from typing import Protocol

class Drawable(Protocol):
    """任何拥有 .draw() 方法的对象都是 Drawable"""
    def draw(self) -> str: ...

class Circle:
    def draw(self) -> str:
        return "⚪ 画圆"

class Square:
    def draw(self) -> str:
        return "⬜ 画方"

class Car:
    def run(self) -> str:  # 没有 draw 方法
        return "🚗 跑"

def render(obj: Drawable) -> None:
    print(obj.draw())

render(Circle())  # ✅ 正确:Circle 有 draw 方法
render(Square())  # ✅ 正确:Square 有 draw 方法
render(Car())     # ❌ 类型检查报错:Car 没有 draw 方法

Practical combat: log interface (does not depend on specific log library)

from typing import Protocol

class Logger(Protocol):
    def info(self, msg: str) -> None: ...
    def error(self, msg: str) -> None: ...

def process_data(data: dict, logger: Logger) -> None:
    logger.info(f"开始处理: {len(data)} 条")
    # ... 业务逻辑 ...
    logger.error("处理失败")

# 不需要继承:任何有 info/error 的对象都能用
import logging
process_data({"a": 1}, logging.getLogger("app"))  # ✅ Python 自带的 logger 满足协议

Advantages: You no longer need to write complex inheritance for unit test mocks - as long as the object satisfies the protocol signature. This is also Iterable / Callable How the built-in protocols work.

3. TypedDict:Add type to dictionary (farewell **kwargs random connection)

from typing import TypedDict, NotRequired  # NotRequired: Python 3.11+

class UserInfo(TypedDict):
    name: str
    age: int
    email: NotRequired[str]  # 可选字段

def create_user(info: UserInfo) -> None:
    print(f"用户: {info['name']}, 年龄: {info['age']}")

# ✅ 正确使用
create_user({"name": "Alice", "age": 30})
create_user({"name": "Bob", "age": 25, "email": "bob@example.com"})

# ❌ 类型检查报错:缺少 age 字段
create_user({"name": "Charlie"})

contrast @dataclass and TypedDict

sceneUse dataclassUse TypedDict
Data structure comes from external (JSON/API)❌ Need to convert manually✅Natural match
Need method logic
Need to dynamically increase or decrease fieldstotal=False support
ORM/Schema definitionMultipurposeRPC calls are easy to use
# 与 JSON API 天然配合
class ApiResponse(TypedDict):
    code: int
    data: dict
    message: str

def parse_response(resp: str) -> ApiResponse:
    import json
    return json.loads(resp)  # ✅ 返回的类型被正确注解

🧪 Usage example: Comprehensive practical combat

A practical scenario using today's three knowledge points - a type-safe configuration loader:

from typing import TypeVar, Protocol, TypedDict, Any
import json

# 1. TypedDict 定义配置结构
class DatabaseConfig(TypedDict):
    host: str
    port: int
    username: str
    password: str

class AppConfig(TypedDict):
    debug: bool
    database: DatabaseConfig
    allowed_origins: list[str]

# 2. Protocol 定义配置源接口
class ConfigLoader(Protocol):
    def load(self, path: str) -> dict[str, Any]: ...

# 3. TypeVar 写泛型反序列化
T = TypeVar("T")

def load_config(loader: ConfigLoader, path: str, model: type[T]) -> T:
    raw = loader.load(path)
    return model(**raw)  # type: ignore  # TyepDict ** 展开

Test using Mock to directly pass a common object(Does not inherit anything, relies entirely on Protocol):

class MockLoader:
    def load(self, path: str) -> dict:
        return {"debug": True, "database": {"host": "localhost", "port": 5432, "username": "root", "password": ""}, "allowed_origins": []}

loader = MockLoader()
config = load_config(loader, "config.json", AppConfig)
print(config["debug"])  # True

⚠️ Precautions / Pitfall avoidance guide

Pitfallsillustratesolve
Type annotations only take effect during static checkingTypeVar Type checking will not be done at runtime, and no exception will be thrown if the wrong type is passed in.add isinstance Check or use pydantic
TypeVar Binding vs Constraintbound=SomeClass = must be a subclass of the specified class;int, str = can only be of these specific typesClear semantics: for subclasses bound, the enumeration uses constraints
Protocol's structure matching is "safe" but not "complete"Complies as long as the signature matches, no semantics are checkedMake good comment documentation, unit testing is still important
TypedDict of ** Expandmodel(**raw) An error may be reported during type checking because TypedDict not real dictadd # type: ignore or use pydantic Model
NotRequired requires Python 3.11+Old version does not haveAvailable from 3.8-3.10 total=False Control the entire TypedDict

total=False Alternative (compatible with Python 3.8+):

class PartialConfig(TypedDict, total=False):
    debug: bool
    host: str
    port: int
# 所有字段都是可选的

📝 Summary

  • TypeVar: Let your function type follow the input type, goodbye Any pollute
  • Protocol: Formal ID card of duck type, decoupled interface and implementation, zero intrusion of Mock
  • TypedDict: Allows JSON/dict data to have precise shape definition, making API docking safer

Many people say that Python's type annotations "cannot change the runtime behavior even if they are written." That's true, but its real value is:After you type it mypy or pyright In the moment of inspection, a batch of potential bugs are moved from runtime to coding time. For yourself who comes back to change the code after 3 months, a good type signature is the best document.