# 隐患与惊喜并存的 .pth (July 17, 2025)

`.pth` 有个[饱受争议](https://github.com/python/cpython/issues/78125)的点，就是它允许执行任意代码（参考 [site](https://docs.python.org/3/library/site.html) 文档）

### .pth 是干嘛的
`.pth` 文件是一类文本文件，如果放在 site-packages 目录下，python 解释器在启动阶段，会读取并解释其内容。`.pth`的内容很简单，一行行的，每行只能是以下三者之一：

1. **一个路径，这个路径会被添加到 **`sys.path`** 中。** 有一定 python 经验的人应该都了解过 `sys.path` 是什么。简单来说就是加载 module 的根目录列表。
2. **一个 import 开头的语句，这个语句会被 **`exec()`** 执行。** 本质上其实允许了执行任意代码
3. 一个 `#` 开头的行，就是注释，跳过。

<img src="/nlark/yuque/0/2025/png/26070246/1752759045252-27754167-7023-45ce-8805-f4da3bffa712.png" width="1056" title="" crop="0,0,1,1" id="u5316be90" class="ne-image">

其中这个第二点就是比较危险，比如有害的第三方库如果骗你安装了，就可以在你每次正常启动 python 时，在执行你常规代码之前，进行一些 monkey patching 的操作

当然你安装一个包当然就是信任它了，所以怪不得别人 😂 只是想提醒大家，你安装一个包之后即使不用命令行或者 import 的方式调用它，它也可能可以让你在不经意间执行它。比起更广为人知的通过 sdist 在 build 阶段执行人代码的安全问题，这个方式更鲜为人知。

---

但是这篇文章不是想讲这些的，而是想讲讲我发现了这个功能可以有一些很好的用法

### 好的用途
加载环境变量很讨厌。要么你安装 `python-dotenv` 然后用 `dotenv run` 启动你的项目，要么在某个最早调用的模块里调用 `dotenv.load_dotenv(override=True)`，如果你只想在开发阶段加载 `.env`（因为生产环境一般直接在平台上设置环境变量，不通过 `.env` 文件），可能还得放在一个 try-expect 块中，甚至写上这样的模板代码：

```python
from contextlib import suppress

with suppress(ImportError):
    from dotenv import load_dotenv

    load_dotenv(override=True)
```

多丑陋！而且这样会多出一个令人疑惑的 `__init__.py`（因为你往往希望尽可能早地进行这个调用）。在我写 [hmr](https://github.com/promplate/hmr) 之前，还涉及到一个 overhead 的问题：contextlib 不是一个很轻的标准库。

今天我写了一个超小的库：[CNSeniorious000/dotenv.pth: load `.env` when python starts - GitHub](https://github.com/CNSeniorious000/dotenv.pth)

当你安装了这个库，每次你任何方式启动 python 或者用什么 cli 工具，它都会自动 load 环境变量。这意味着如果你只想在开发环境中加载 .env，你只需要把 `dotenv.pth` 装在 dev dependencies 中就行了。

---

另一个例子是，我的 [hmr-daemon](https://github.com/promplate/hmr/blob/649181d4cad3776158c92f778ef511f3596f514e/packages/hmr-daemon/hmr_daemon.py#L55)，它现在还没有任何文档，但我可以在这里简单说一下：

+ 它启动一个 daemon 线程，注册 [hmr](https://pypi.org/project/hmr/) 中的 ModuleFinder，使得所有当前项目的模块都是 ReactiveModule
+ 但是所有 reload 都在这个线程中执行，所以你看到的效果就是，如果你的代码还在运行，这时候你保存其中某个文件（比如更新一个函数的实现，或者比如更改某个参数），立即就能看到效果

常用的场景是，你想用 python 的 shell（或者 ipython 之类的）来交互式地调试你的项目，但是当你修改项目文件后，你肯定懒得关闭再开启 shell 一遍。

通过 `hmr-daemon`，你不用重启 shell，所有 `from module import value` 得到的 value 都是最新的。或者 `module.value` 这样的也是最新的。



### 一个坑
开发过程中往往会在虚拟环境中开发，但是将虚拟环境的 site-packages 添加到 sys.path 中的逻辑本身也是通过一个 `.pth` 文件实现的，它叫 `_virtualenv.pth`，而 python 执行 `.pth` 是按字典序执行的，所以如果你的 `.pth` 文件以 `_` 开头，就很可能没法 import 一个在虚拟环境中安装了的包，因为这时候虚拟环境可以说还没别“激活”—— 激活虚拟环境的 `.pth` 还没执行呢。

debug 终于发现是这个原有的时候还挺搞笑的 😭



### 总结一下
我认为 `.pth` 是个很 Pythonic 的特性，很鲜明地展现了 Python 的个性 —— 极高的自定义性。可以用它实现很多提高 DX（developer experience）的小功能。

而且我认为这个东西并没有被人开发得很完全，我想我可能未来也会开发更多基于 `.pth` 的有意思的库。



> 比如我现在想到的几个是：
>
> + 在 Windows 上 Python 开发有个很坑人的点，它文件的默认编码是 GBK 而不是 utf-8，所以到处需要指定。我们可以在 `.pth` 中 monkey patch 这个
> + 可以监测 .env 的变化，然后随时更新到当前进程的环境变量（不过这个 hmr 本身也快要能实现了：它现在的依赖图不止支持 python 模块也支持任意文件了，正在 [reactive-fs](https://github.com/promplate/pyth-on-line/blob/6fe4818a7453f808679891785175fd3ddd56cb39/packages/hmr/reactivity/hmr/fs.py) 分支积极开发中，说起来这个功能可能最早受启发于[这条评论](https://github.com/fastapi/fastapi/discussions/13192#discussioncomment-11796233)）
> + 像 [nested-asyncio](https://pypi.org/project/nest-asyncio/) 这样的“只暴露一个无参函数”的包，完全可以发布一个 `.pth` 包，这样不用在代码中显示地 patch，而是在依赖中这么做
> + …… 能想到的还有很多，但今天挺晚了，我先回去睡觉了
>