Laws 法条图鉴¶
这些法条不会替你写业务代码,只是在你准备“越界”的那一刻吹响法槌。
PyCourt 当前内置 15 条 Law,分别从架构、类型、风格、时间与数据等维度,为你的项目提供一套“最低限度的文明约束”。
你不需要一次性全记住它们。把这页当成“法条菜单”就好:
当某类问题开始频繁出现时,再回来点开对应的 Law,慢慢加重审查即可。
如果你对某条 Law 有很多话想说——踩坑经历、重构故事、灰色边界讨论—— 欢迎按照参与共创指南提交一篇“法条解读”或“判例故事”, 未来我们会为每条 Law 挂出 1–2 篇社区精选解读,让新来的开发者看到的不只是“禁止条文”,还有“为什么值得这样禁止”。
架构与边界安全¶
关注“模块和模块之间,到底该怎么互相看”的一组法条。
BC001 边境管理¶
关键词:HTTP 边界 / 适配器 / DTO
禁止在 HTTP 层和适配器层直接使用原始类型(dict、str、Any 等)或随手拼凑的容器类型,
以避免在系统边缘泄露内部模型,实现层不小心直接暴露给外部世界。
- 它在看什么? 各种
api/routes/、infra/adapters/**下的函数签名与返回值。 - 你为什么会被抓? HTTP handler 直接收发
dict[str, Any],把内部模型原样扔进响应。 - 怎么和它和解? 给入口/出口一份清晰的 DTO / 契约类型,内部怎么折腾它管得不多。
DI001 依赖倒置¶
关键词:跨层 import / 组合根 / 反向依赖
禁止业务模块绕过核心抽象,直接跨层 import 具体实现,以避免模块之间纠缠成一团,彻底打穿依赖倒置。
- 它在看什么? 从
api/app等边缘层偷偷伸进infra、core的“奇怪导入”。 - 你为什么会被抓? 在控制器里直接 import 仓储实现,而不是通过 Port / Service 接口。
- 怎么和它和解? 把跨层 wiring 放进组合根(DI 容器、
app/main.py等),边缘层只面向抽象。
UW001 工作单元¶
关键词:事务边界 / 仓储 / UoW
禁止在仓储内部调用 commit() / rollback() 之类的事务控制,以避免工作单元的原子性被悄悄破坏。
- 它在看什么? 各种
repository、infra/database模块里对事务 API 的直接调用。 - 你为什么会被抓? 某个仓储方法里顺手
session.commit(),把调用方彻底绑死在一个实现上。 - 怎么和它和解? 让仓储只做数据访问,把事务控制留在 UoW 或服务层,保证一整个业务操作要么全部成功,要么全部回滚。
RE001 聚合导出¶
关键词:__init__.py / 包外观 / 隐形实现层
禁止把复杂逻辑、隐藏依赖塞进 __init__.py 里当作“导出技巧”,
以避免包外观退化成第二个实现模块,调试与审计都变得混乱。
- 它在看什么? 各种包的
__init__.py,尤其是里面的 import 和逻辑。 - 你为什么会被抓? 把真实逻辑写在
__init__.py中,再导出给其他模块使用。 - 怎么和它和解?
__init__.py只负责聚合导出已经存在的模块符号,不承担实现职责。
类型与契约安全¶
关注“值到底是什么?契约写没写清楚?”的一组法条。
AC001 鸭子类型¶
关键词:Any / dict / cast
禁止依赖 Any、未知 dict 和随手 cast,
以避免将核心数据流变成类型系统无法保护的“鸭子类型迷雾”。
- 它在看什么? 各种
dict[str, Any]、神秘的cast(XXX, something)。 - 你为什么会被抓? 为了省事,所有 API 都接受
dict,内部到处是["foo"]["bar"]。 - 怎么和它和解? 用 Pydantic 模型 / dataclass / TypedDict 等给数据一个清晰身份。
OU001 裸对象类型¶
关键词:object / 模糊类型 / 灰色物质
禁止在核心代码中使用“裸 object”或同等模糊的类型签名,
以避免出现一堆“你说它是什么它就是什么”的灰色值,没人敢重构。
- 它在看什么? 函数参数/返回值标注为
object/dict/str | object之类的奇葩组合。 - 你为什么会被抓? 把关键参数类型简单写成
object,希望“将来再细化”,结果就一直没下文。 - 怎么和它和解? 至少给重要对象一个领域名:哪怕是
UserId这样的 wrapper,也比object好得多。
PC001 参数分类¶
关键词:业务规则 / 技术常量 / RuleProvider
禁止把业务上需要调整的规则(阈值、权重、比例)当成“技术常量”写死在代码里,
以避免把“运营调参”变成“改代码上线”的低效流程。
- 它在看什么? 各种看起来不像技术常量的数值/字符串常量,特别是在
core/constants/外面。 - 你为什么会被抓? 在代码里直接写
TOP_K = 5、SCORE_THRESHOLD = 0.7,而不是走配置或规则表。 - 怎么和它和解? 把可调规则放进专门的 RuleProvider / 配置系统,技术常量只存放纯技术性的固定值。
TC001 内部盘查(TYPE_CHECKING)¶
关键词:if TYPE_CHECKING: / 仅类型导入 / 循环依赖
禁止滥用 if TYPE_CHECKING: 把真实的模块依赖藏在“只给类型检查看的”分支里,
以避免循环依赖被悄悄埋进类型注解中,导致结构腐化。
- 它在看什么? 所有
if TYPE_CHECKING:块以及里面的导入。 - 你为什么会被抓? 通过“类型导入”偷偷实现跨层依赖,然后在运行时再想办法兼容。
- 怎么和它和解? 类型注解也属于架构的一部分,避免借它绕过正常的依赖边界。
SK001 技能提供者¶
关键词:技能系统 / 资源目录 / 提供者模式
禁止业务代码直接对 assets/skills 等物理目录进行硬编码访问,
以避免核心逻辑与文件布局紧耦合,难以演进和替换。
- 它在看什么? 所有直接访问技能资源目录的代码路径。
- 你为什么会被抓? 在业务函数里
open("assets/skills/xxx.json")之类的硬路径。 - 怎么和它和解? 通过统一的 SkillId 注册表和 provider 访问技能,逻辑只认识 SkillId,不认识物理路径。
可读性与代码卫生¶
关注“别让未来的你(或你的同事)看不懂现在的你在干嘛”的一组法条。
DS001 文档字符串¶
关键词:docstring / 公共 API / 意图可读
禁止公共函数和类缺少清晰、非平凡的文档字符串,
以避免 API 意图和契约只能靠猜、靠读实现才能理解。
- 它在看什么? 所有对外暴露的函数、类、方法有无合理 docstring。
- 你为什么会被抓? 公共接口只有一个函数名,参数一长串,注释要么没有,要么写“do something”。
- 怎么和它和解? 一两句话说明“做什么、不做什么、有何前置条件”,比写十行实现更有价值。
LL001 过度复杂¶
关键词:超长函数 / 深层循环 / 纠缠逻辑
禁止函数长度或循环复杂度超过约定阈值,
以避免出现“一看就想关电脑”的巨型过程。
- 它在看什么? 超过合理长度的函数、多层嵌套 if/for、长得像脚本的业务代码。
- 你为什么会被抓? 一个函数里什么都干:校验、查询、拼数据、发请求、写日志、处理异常……
- 怎么和它和解? 把过程拆成几个有名字的步骤,让法官和同事都看得懂你的意图。
HC001 硬编码¶
关键词:魔法数字 / 业务字面量 / 常量仓
禁止在受控常量模块之外使用硬编码的业务字面量和魔法数字,
以避免重要阈值和业务规则散落在一堆 if/for 里,被遗忘在角落。
- 它在看什么? 到处出现的
"vip","normal",0.8,5000之类奇怪数字/字符串。 - 你为什么会被抓? 为了图方便,直接在条件里写
"gold"、top_k = 10,过一阵子谁也记不清含义。 - 怎么和它和解? 把这些东西搬进明确的常量模块或配置系统里,给它们一个名字和注释。
时间与数据一致性¶
关注“程序在时间轴上、数据流上是不是靠谱”的一组法条。
DT001 时间漂移¶
关键词:datetime.now() / 时区 / 可测试性
禁止在业务代码里直接使用 datetime.now() / utcnow(),
以避免时间相关逻辑难以测试、线上线下行为不一致。
- 它在看什么? 所有直接调用
datetime.*now()的位置。 - 你为什么会被抓? 在业务逻辑中到处读当前时间,却不给测试任何控制手段。
- 怎么和它和解? 通过集中式的 TimeProvider / Clock 接口获取时间,让测试可以注入“现在”。
TP001 测试代码¶
关键词:测试纯度 / 重 I/O / 假单测
禁止在标称为“单元测试”的地方偷偷引入重 I/O、复杂依赖,
以避免出现又慢又脆、却测不出真正问题的“伪单测”。
- 它在看什么? 测试代码中对数据库、外部服务、庞大依赖图的直接使用。
- 你为什么会被抓? 一个 test 函数既连数据库,又跑网络,还依赖全局状态。
- 怎么和它和解? 把重 I/O 留给集成/E2E 测试,单测专注于小而清晰的行为单元。
VT001 向量触发器¶
关键词:向量搜索 / 事件 / 能力边界
禁止触发路由发出当前配置下根本无法履行的向量请求或事件,
以避免“法官口头宣判了一堆向量行动”,但运行时没有任何实际能力支撑。
- 它在看什么? 向量相关的触发点、事件路由与实际 provider 配置之间是否匹配。
- 你为什么会被抓? 在代码里发出了某个向量事件,但没有任何 provider 能处理它。
- 怎么和它和解? 确保向量触发配置与 provider 实现一致,不做“空头承诺”。
读完这页,你不需要立刻记住所有条文。
更实际的用法是:选你现在最头疼的那一类问题(比如硬编码、边界乱、时间漂移),先打开对应的 2–3 条法条,慢慢加到你的项目里。
等 PyCourt 在你的日常工作流里站稳脚跟之后,再考虑“全院审判”。