📌 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 function、Protocol (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:
| scene | Use dataclass | Use TypedDict |
|---|---|---|
| Data structure comes from external (JSON/API) | ❌ Need to convert manually | ✅Natural match |
| Need method logic | ✅ | ❌ |
| Need to dynamically increase or decrease fields | ❌ | total=False support |
| ORM/Schema definition | Multipurpose | RPC 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
| Pitfalls | illustrate | solve |
|---|---|---|
| ❌ Type annotations only take effect during static checking | TypeVar 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 Constraint | bound=SomeClass = must be a subclass of the specified class;int, str = can only be of these specific types | Clear 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 checked | Make good comment documentation, unit testing is still important |
❌ TypedDict of ** Expand | model(**raw) An error may be reported during type checking because TypedDict not real dict | add # type: ignore or use pydantic Model |
| ❌ NotRequired requires Python 3.11+ | Old version does not have | Available 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, goodbyeAnypolluteProtocol: Formal ID card of duck type, decoupled interface and implementation, zero intrusion of MockTypedDict: 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
mypyorpyrightIn 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.