C++ 和 Java 要求声明类型, 并在编译期间检查这些类型。 Python runtime 只有在某个操作真正执行时才检查类型。 到目前为止的示例几乎完全没有类型声明, 对于小程序,你可能根本不会觉得缺少了什么。

Python 3.5(2015)引入了 type hint, 外观类似静态类型语言中的类型声明。 Python runtime 会忽略 type hint。 只有当某些代码主动请求它们时,Python 才会求值它们。 如果你希望获得像 typed language compiler 那样的静态类型检查, 必须另外运行独立的 type-checking tool (本书使用 Astral 的 ty)。

渐进式类型

你可以一次只给一个函数增加 type hint。 没有 annotation 的代码仍然可以正常工作。 type checker 会把这类代码视为 Any 类型, 而 Any 与所有类型都兼容。 因此,有类型和无类型的代码可以共存。 这就是渐进式类型(gradual typing)。 你可以逐渐只在真正值得的地方增加 hint:public interface、 复杂的数据,以及其他人依赖的代码。 显式写出的 Any 表示这个值确实是动态的。 ty 会把“由于缺失 annotation 而产生的 Any”报告为 Unknown, 以区别于你自己明确写出的 Any。 两者行为相同:都与所有类型兼容。

Any 和 object 并不相同。 两者都可以接受任何值, 但拿到一个 object 后,类型系统并不保证它具有任何行为, 因此 type checker 会拒绝你试图对它执行的所有操作。 Any 则既接受任何值,又允许任何操作, 所以它本质上是“退出类型检查”,而不是一个很宽泛的安全类型。

Type Hint

hint 可以 annotation 参数、返回值或变量。 参数和变量使用冒号,返回类型使用箭头:

# typed_basics.py

def repeat(text: str, times: int) -> str:
    return text * times

print(repeat("ab", 3))
#: ababab

total: int = 0
for word in ["a", "bb", "ccc"]:
    total += len(word)

print(total)
#: 6

容器和 optional type 基本按你口头描述的方式书写:list[int]、 dict[str, float]、tuple[int, ...], 而 str | None 表示“一个字符串,或者什么也没有”。 不返回任何值的函数声明为 -> None, 这就是为什么本章所有代码清单里的 __init__() 都这样结束。

类型检查器:ty

这些 hint 自己不会做任何事情。 需要一个工具来检查它们:

ty check

uvx ty check 可以无需安装直接运行它, 而 uv tool install ty@latest 会把它安装到你的 path 上。

当 hint 和代码不一致时,它会报错; 两者一致时,它保持安静。 本书会用这种方式检查每一个可运行示例。 build 会在每次修改时运行 ty, 所以你在这里读到的代码不仅能够运行,也能通过类型检查。

捕获错误

类型检查可以在程序运行之前发现错误。 例如:

# area.py
def area(width: int, height: int) -> int:
    return width * height

# ty: argument of type "str" is not assignable to "int":
print(area("3", 4))  # type: ignore
#: 3333

在 runtime 中,area("3", 4) 不会报错。 它返回 "3333",因为 "3" * 4 会把字符串重复四次。 bug 会在之后某个位置才暴露,而且经常离真正造成问题的那一行很远。 type checker 则会立刻发现问题。