本地启动 Python 服务时,配置通常来自两处:终端已经存在的进程环境,以及项目中的 .env 文件。python-dotenv 负责解析 .env,并把其中的键值写入 os.environ。实际使用中的几个边界是文件格式、变量优先级、查找路径和进程生命周期。
基本用法
安装
pip install python-dotenv创建 .env 文件
# .env 文件示例
DEBUG=True
SECRET_KEY=mysecretkey123
DATABASE_URL=postgres://user:password@localhost:5432/mydb
- 注释用 #
- 每行格式:KEY=VALUE
- 带空格或特殊字符的值可用引号 “value”
加载环境变量
使用 load_dotenv() 默认 加载 .env 文件
from dotenv import load_dotenv
import os
load_dotenv()
debug = os.getenv("DEBUG")
secret_key = os.getenv("SECRET_KEY")
指定 .env 文件路径
from dotenv import load_dotenv
from pathlib import Path
import os
env_path = Path('.') / '.env'
load_dotenv(dotenv_path=env_path)
database_url = os.getenv("DATABASE_URL")
读取为字典(不写入系统环境变量)
from dotenv import dotenv_values
config = dotenv_values(".env")
print(config['DEBUG'])
如果环境变量已经在系统中存在,
load_dotenv默认不会覆盖它。
传入override=True后,.env中的值会覆盖已有进程环境:
load_dotenv(override=True)类型转换
DEBUG = os.getenv("DEBUG") == "True"
PORT = int(os.getenv("PORT", 8000)) # 默认值 8000环境变量的覆盖顺序
load_dotenv() 的函数签名默认是 override=False。按照 python-dotenv 文档,同名变量的读取顺序是:
| 调用方式 | 优先级从高到低 |
|---|---|
load_dotenv() | 进程环境 → .env → 默认值 → 空字符串 |
load_dotenv(override=True) | .env → 进程环境 → 默认值 → 空字符串 |
例如,终端已经存在:
export MODEL_PROVIDER=openai项目的 .env 又写了:
MODEL_PROVIDER=zhipuload_dotenv()
print(os.getenv("MODEL_PROVIDER"))
# openai
load_dotenv(override=True)
print(os.getenv("MODEL_PROVIDER"))
# zhipu在服务端部署中,进程环境通常保留更高优先级;本地脚本需要固定复现实验配置时,才会明确使用 override=True。
.env 文件从哪里加载
没有传入路径时,load_dotenv() 会调用 find_dotenv() 查找 .env。显式路径可以避免启动目录变化后读取到另一份文件:
from pathlib import Path
from dotenv import load_dotenv
project_root = Path(__file__).resolve().parents[1]
load_dotenv(project_root / ".env")通过 flask run 启动时,Flask 也会加载 .env 和 .flaskenv。它的 CLI 文档 记录了这一层优先级:命令行环境高于 .env,.env 高于 .flaskenv;查找从执行 flask 命令的目录向上进行。
.env 中每个变量单独占一行:
# 模型服务配置
MODEL_PROVIDER=provider-name
MODEL_NAME=model-name
PROVIDER_API_KEY=<YOUR_API_KEY>下面的写法会让整行成为注释:
# MODEL_PROVIDER=provider-name MODEL_NAME=model-name修改 .env 后为什么还是旧值
.env 只是进程启动或执行 load_dotenv() 时读取的输入文件。已经运行的 Python 进程不会因为文件内容变化而自动替换 os.environ。
代码热重载和环境刷新也是两件事:
修改 Python 文件
└── reloader 检测到变化,重启 Web 进程
修改 .env
└── 不等于所有运行进程都会重新读取环境Flask 与 Celery 分别运行时,它们各自保存一份进程环境。只重启 Flask,后台 Worker 仍可能使用旧值。VS Code、Shell、Docker Compose 和系统服务也可能在启动子进程前注入同名变量,load_dotenv(override=False) 会保留这些值。
不输出密钥的排查方式
排查重点是加载了哪份文件、变量是否存在,而不是把值打印到终端:
import os
from dotenv import find_dotenv
dotenv_path = find_dotenv()
print("dotenv file:", dotenv_path or "<not found>")
print("provider configured:", bool(os.getenv("MODEL_PROVIDER")))
print("api key configured:", bool(os.getenv("PROVIDER_API_KEY")))常见现象可以对应到不同层级:
| 现象 | 检查位置 |
|---|---|
| 一直读取旧 Provider | 终端或启动器是否已有同名环境变量 |
| Flask 生效,Celery 没生效 | Worker 是否单独重启 |
| 本地脚本找不到变量 | 当前工作目录和 find_dotenv() 结果 |
| 配置看起来存在但读不到 | 是否被 # 注释、是否一行写了多个变量 |
| Key 已更换但请求仍使用旧 Key | 旧进程是否仍在运行 |
文件与日志的安全边界
- 不要将 .env 上传到版本控制(加入 .gitignore)。
- 提供 .env.example 示例文件,列出所需的变量名称,但不包含实际值。
- 可以为不同环境创建不同 .env 文件:.env.dev / .env.prod。
- 日志只记录变量是否存在,不记录 Token、Cookie、密码和私钥的完整值。
.env中的 Key 一旦出现在公开日志、截图或工单中,需要作废并重新生成。