---
date: 2026-05-11
category: python / importlib
severity: low
recurrence: high  # 任何 importlib 加载含 dataclass 的模块都会遇到
---

# ERR-20260511-001 · Python 3.12 importlib + `@dataclass` 反查 `sys.modules` 报 NoneType

## 症状

用 `importlib.util.spec_from_file_location` + `module_from_spec` + `exec_module` 动态加载一个 **含 `@dataclass` 装饰器** 的 Python 模块时，Python 3.12 抛：

```
AttributeError: 'NoneType' object has no attribute '__dict__'. Did you mean: '__dir__'?
  File ".../dataclasses.py", line 749, in _is_type
    ns = sys.modules.get(cls.__module__).__dict__
```

栈走到用户的 `@dataclass` 行就崩。

## 根因

`importlib.util.module_from_spec(spec)` **不会把模块注册到 `sys.modules`**。
Python 3.12 的 `dataclasses._is_type` 内部用 `sys.modules.get(cls.__module__).__dict__` 反查模块——拿不到 → `None.__dict__` → AttributeError。

3.11 及以前的 `_is_type` 实现不依赖这条路径，所以同样代码在老版本不报。

## 解法（一行修复）

`exec_module` 之前手动注册：

```python
def _load(name: str, path: Path):
    spec = importlib.util.spec_from_file_location(name, path)
    mod = importlib.util.module_from_spec(spec)
    sys.modules[name] = mod          # ← 关键这行
    spec.loader.exec_module(mod)
    return mod
```

注册的 `name` 必须跟 `spec_from_file_location` 第一个参数一致。

## 适用场景

文件名带连字符（`ai-review-stats.py` / `weekly-review.py`）的脚本不能 `import` 只能走 importlib。
仓库里 `scripts/ops/` 下多个 python 工具都用这种命名，未来任何"消费者复用核心解析逻辑"都会撞上。

## 替代方案 vs 现选

| 方案 | 评估 |
|---|---|
| ✅ 一行注册到 sys.modules | 选这个，最小侵入 |
| 重命名脚本为 `ai_review_stats.py` 让能直接 import | break change，需要改所有引用方 + git mv |
| 抽 `_ai_review_common.py` module | 工作量大，现在只有 2 个消费者 ROI 不划算 |

待第 4 个消费者出现时再评估是否抽 module。

## 触发记录

- 2026-05-11: `scripts/ops/ai-review-auto-eval.py` 加载 `ai-review-stats.py`（含 `@dataclass class Finding/ReviewEntry/PRAggregate`），首次踩坑 → 本文档
