本地启动 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=zhipu
load_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 一旦出现在公开日志、截图或工单中,需要作废并重新生成。