资讯详情

python-dotenv 用法详解:环境变量加载、覆盖规则与安全实践

📅 2026/10/22 16:44:17 | 华诺云谱 👁 阅读
python-dotenv 用法详解:环境变量加载、覆盖规则与安全实践
不知道你有没有遇到过这样的场景项目里数据库密码、第三方 API 的 Key、对象存储的 AccessKey 直接以字符串形式写死在代码文件里。本地跑没有任何问题但代码一旦提交到仓库这些密钥就跟着走了。我见过不止一个团队因为这种习惯上线后不久就被自动化脚本扫描到硬编码的云服务密钥测试环境被拖库、资源被刷爆排查起来无比痛苦。那之后我给自己定了一条规矩任何 Python 项目启动的第一步不是写业务逻辑而是先把环境变量的加载链路搭好。python-dotenv 就是这条链路里最顺手、生态最成熟的一个工具。这篇文章我想完整聊一聊 python-dotenv 的用法不只是讲load_dotenv()怎么调用而是把配置文件的优先级、覆盖规则、多环境管理、安全边界和实际排坑经验都过一遍。无论你是刚接触 Python 的新手还是在团队项目里被环境变量问题困扰过的开发者这篇都能给你一份可以直接照着用的操作手册。我踩过的坑不会再让你踩一遍。1. 为什么每个 Python 项目都需要一个加载环境变量的环节1.1 配置和代码分离这个老生常谈的问题到底在谈什么先想一个问题同一个应用开发环境连本地数据库测试环境连 staging 库生产环境连线上主库。三套地址、三套账号密码代码逻辑却完全一样。如果你把数据库连接串直接写在.py文件里那么换一个环境就要改一次代码改完还得重新走一遍发布流程万一哪个环境漏改了后果可能就是测试环境误连了生产库。环境变量这种机制就是为了解决这个问题而存在的。它把配置从代码里抽离出来程序运行时去读操作系统的环境变量而不是读自己的源码文件。这样同一套代码打包到任何环境只要提前在环境里设置好对应的变量程序就能正常工作。这也是业界常说的配置与代码分离的核心思想代码管逻辑环境管参数。但环境变量也有一个实际的痛点本地开发时你得在终端里手动敲一堆export DATABASE_URLxxx每次打开新终端都得重新设置项目成员多了之后每个人记的变量还不一样内存里充满了各种版本的配置。这种重复劳动很快就会让人崩溃于是就有了把环境变量写进文件、由程序自动加载的需求。1.2 dotenv 到底帮你做了什么python-dotenv 做的事情可以用一句话概括它把一个名为.env的文件逐行解析出来然后把里面的键值对写进os.environ这个字典里。之后你的代码就可以用os.getenv()去读取这些配置了。用最直白的话说它就是把手动敲 export 命令这件事自动化了。你只需要维护一个.env文件里面写清楚每个变量的值启动程序时调用一次load_dotenv()所有配置就都躺在os.environ里等着被读取了。这里有个容易忽略的原理os.environ本质上就是一个字典里面保存着当前进程的全部环境变量。子进程会继承父进程的环境变量但你用os.environ[KEY] value设置的值只对当前进程及其之后启动的子进程生效不会修改你操作系统里真正持久化的环境变量配置。dotenv 做的就是这个内存级的设置程序退出后一切恢复原样这恰恰是它适合本地开发的原因。2. 五分钟接入安装、导入与第一次读取2.1 安装与最小可用示例安装很简单一个命令的事pip install python-dotenv如果你用的是 Poetry 或者 uv 这类依赖管理工具直接把它加进项目依赖即可。装好之后在项目根目录创建一个.env文件内容就是普通的键值对格式# .env DATABASE_URLpostgresql://app_user:app_pass127.0.0.1:5432/app_db API_KEYsk-1234abcd DEBUGtrue然后在你的入口文件里加载它# main.py import os from dotenv import load_dotenv load_dotenv() print(os.getenv(DATABASE_URL)) print(os.getenv(API_KEY))运行python main.py你会看到终端里打印出.env文件里对应的值。整个接入过程不超过两分钟这就是 dotenv 在 Python 生态里如此流行的原因心智负担极低。2.2 load_dotenv 和 os.getenv 的关系很多初学者会困惑load_dotenv()和os.getenv()到底是啥关系其实它们是两件事。load_dotenv()负责把.env文件里的内容灌入到os.environos.getenv()只是从os.environ里读取值。你可以把load_dotenv()理解为往仓库里进货os.getenv()是从仓库里取货少了前面那一步后面取到的永远是None。这也是最常见的初学者报错明明.env文件写了值os.getenv(DATABASE_URL)却打印出None。大概率是你没有调用load_dotenv()或者调用的位置在读取操作之后。load_dotenv()必须在所有读取环境变量的代码之前执行通常放在模块导入之后的第一行。另外一个小建议读取环境变量时尽量用os.getenv(KEY)而不是os.environ[KEY]。两者的差别在于key 不存在时os.getenv返回None而os.environ[KEY]会直接抛KeyError。对于配置项比较全的项目抛异常其实是一种快速暴露问题的好方式但如果你只是想给某个配置设个默认值那么os.getenv(DEBUG, false)这种写法就舒服得多。2.3 还没完find_dotenv 与路径搜索load_dotenv()默认会在当前工作目录下找.env文件。这里有个暗坑所谓当前工作目录不一定是你项目代码所在的目录而是你执行python命令时所在的目录。举个例子你的项目在/home/user/myproject但你习惯在/home/user下执行python myproject/main.py。这时load_dotenv()会在/home/user目录下找.env找不到就什么都不做你的环境变量自然全是None。解决方式有两个。第一个是显式指定路径from dotenv import load_dotenv from pathlib import Path BASE_DIR Path(__file__).resolve().parent load_dotenv(BASE_DIR / .env)用Path(__file__).resolve().parent拿到当前 py 文件所在目录再拼上.env这样无论你从哪里执行脚本都能正确找到配置文件。第二个是使用dotenv.find_dotenv()它会自动向上级目录搜索找到最近的.env文件from dotenv import load_dotenv, find_dotenv load_dotenv(find_dotenv())我个人在开发小型脚本时倾向于第二种代码简洁在维护正式项目时倾向于第一种显式路径一目了然团队成员看到就知道配置文件在哪里。3. 覆盖规则与查找路径让配置文件按预期工作的关键3.1 默认的不覆盖策略这是 python-dotenv 最容易被误解的一个设计。默认情况下load_dotenv()不会覆盖掉操作系统中已经存在的同名环境变量。假设你在终端里已经执行过export API_KEYfrom_shell那么即使.env文件里写了API_KEYfrom_dotenv最终程序读到的还是from_shell。这个设计其实很合理系统环境变量的优先级高于项目配置文件这样可以保证在某些场景下外部注入的配置不会被项目内的文件意外覆盖。但很多第一次接触的人会搞反以为.env文件是最权威的于是出现我改了.env文件程序跑的却还是旧配置的困惑。如果你确实需要让.env文件覆盖已有环境变量可以显式传参load_dotenv(overrideTrue)overrideTrue的含义是以.env文件为准把同名环境变量强制覆盖。真实项目中这个参数要谨慎使用尤其是在多人协作的代码库里如果某个人在启动脚本里加了overrideTrue可能会把 CI 系统注入的环境变量给冲掉引发半夜被叫起来看告警的问题。3.2 多个 .env 文件的加载顺序python-dotenv 并没有自动合并多个.env文件的机制它每次调用只处理一个文件。但你可以通过多次调用同一个函数来实现叠加的效果load_dotenv(.env.base) load_dotenv(.env.local)按上面的顺序执行.env.local里的值会被加载但由于默认不覆盖.env.base里已有的同名变量仍然保持原值。如果你希望.env.local覆盖.env.base第二次调用时就要加overrideTrueload_dotenv(.env.base) load_dotenv(.env.local, overrideTrue)这在多环境配置里是一个实用技巧。基础配置放在.env.base针对本机的个性化覆盖放在.env.local两者分工明确既保证了通用配置的一致性又允许开发者本地微调。3.3 dotenv_values只想读值不想污染环境变量load_dotenv()的副作用是把所有变量写进os.environ。但有些场景你只想知道某个配置值是多少并不想让这些值成为全局环境变量比如你写了一个小工具只想读一次配置用来拼个路径工具本身不需要把配置暴露给子进程。这时可以用dotenv_values()它只解析.env文件返回一个普通字典不碰os.environfrom dotenv import dotenv_values config dotenv_values(.env) print(config.get(API_KEY))它的用法和load_dotenv完全对称也支持encoding和路径参数。对比一下这两个方法的核心区别方法是否写入 os.environ适用场景load_dotenv()是应用启动、脚本初始化后续用os.getenv读取dotenv_values()否一次性读取配置、临时工具、不想污染全局环境3.4 解析规则注释、空行与 export 前缀.env文件不是随便写的它有一套自己的解析规则。首先是注释支持以#开头的行解析时会自动忽略空行也没问题直接跳过。其次是export前缀如果你从 shell 脚本里复制了一段配置过来形如export API_KEYxxxdotenv 也能识别它会自动去掉export再读取键值对。值部分需要注意双引号和单引号会被解析并移除也就是说API_KEYsk-123读出来是sk-123而不是带引号的字符串。值里如果有#符号比如一个备注性质的变量NOTEhello#worlddotenv 会把#当作注释截断读出来只有hello。真要存这种值得用引号包起来NOTEhello#world。4. 多环境配置开发、测试、生产如何共用一套代码4.1 文件命名规范项目跑起来之后第一个绕不开的问题就是开发、测试、生产三个环境的配置不一样怎么管理我见过不少团队把所有环境的值全部写进一个.env然后用变量名的前缀区分比如DEV_DATABASE_URL、PROD_DATABASE_URL代码里再根据当前环境变量去选择。这种方案能用但维护体验很差改一个生产数据库地址得在一堆变量里翻半天。更推荐的做法是按文件拆分.env.development、.env.test、.env.production各管一套启动时通过一个环境变量比如APP_ENV来决定加载哪个文件import os from dotenv import load_dotenv env os.getenv(APP_ENV, development) load_dotenv(f.env.{env}) load_dotenv(.env) # 公共配置最后再补一层这套结构和基础配置 环境覆盖的思路是一致的。无环境变量时默认走 development发布到服务器时设置APP_ENVproduction代码完全不用改。4.2 用 dotenv 的命令行工具给脚本注入环境python-dotenv 在安装时会顺带提供一个名为dotenv的命令行工具支持直接用.env文件来运行其他程序dotenv -f .env.production run python manage.py migrate这条命令的意思是读取.env.production文件把里面的变量注入当前环境然后执行python manage.py migrate。这对于在服务器上跑一次性脚本特别方便不需要手动复制一堆export命令也保证了脚本运行时的配置和项目使用的配置完全一致。如果你用的是 Docker Compose也可以在docker-compose.yml里用env_file直接指定services: app: env_file: - .env - .env.localDocker Compose 的env_file有一个特点如果同一个变量在多个文件中出现后面的文件会覆盖前面的文件。合理利用这个行为你甚至可以在容器编排层面实现基础配置 环境覆盖的组合策略。4.3 CI 里怎么处理 .env很多人在本地开发时已经习惯了.env的存在一到 CI持续集成环境就忘了这回事。CI 的运行环境通常是一个全新的容器里面不会有你的.env文件。在 CI 中正确的做法是把真正的密钥配置在 CI 平台的 Secret 或环境变量面板里然后在 CI 脚本中直接读取系统环境变量或者把这些值动态地写入.env文件再交给应用加载。如果 CI 和本地都走同一套加载逻辑那么代码本身不需要区分环境——本地靠.env文件CI 靠平台注入的系统变量。这就是配置与代码分离在实践中的完整形态代码不知道配置从哪来它只知道读取环境变量。5. 安全红线哪些事 dotenv 管不了5.1 .env 不是加密文件这是我想反复强调的一点.env文件里的内容是纯文本看一眼就能读到。dotenv 只是把文件内容加载到环境变量里它没有任何加密、混淆或者权限保护的功能。很多人因为.env用起来太顺手就误以为它有某种隐藏配置的能力甚至有人把它当成储存生产密钥的安全方案。这是个危险的认知偏差。在本地开发时.env文件属于你的个人环境风险可控但如果这份文件被提交进了代码仓库、被同步到了团队协作平台或者被打包进了发布产物里面的每一个密钥就都暴露了。所以第一步永远是把.env加入版本管理黑名单。在 Git 仓库里创建.gitignore文件明确写上一行.env然后确认这条规则生效。我见过不少我以为已经忽略了的案例——某同事在.gitignore里写的是.env.*结果.env本身反而没被忽略配置照样被提交上去了。这类细节需要反复核实。5.2 .env.example 是团队的底线既然真正的.env不能提交新加入的同事怎么知道项目需要哪些环境变量答案是维护一个.env.example文件它列出所有必需的变量名但值要么留空要么填写无意义的示例值。# .env.example DATABASE_URLpostgresql://user:passlocalhost:5432/app_db API_KEYyour_api_key_here DEBUGfalse新同事拿到代码后执行cp .env.example .env对照示例文件填入自己的本地值就能跑起来。团队里只要坚持维护.env.example就能避免新人不知道要配什么变量的问题。我个人的习惯是每次新增一个环境变量同步更新.env.example并且在代码评审时检查这一项让这条规则变成团队约定。5.3 生产环境的正确姿势生产环境的密钥管理dotenv 不是主角。服务器上的真实密钥应该通过云平台的密钥管理服务、容器平台的 Secret 机制或者专门的密钥管理系统来保存应用启动时由平台注入为系统环境变量。你的代码只需要负责读取不需要关心密钥存放在哪里。那生产环境还需要 dotenv 吗我的经验是需要但只用于非敏感的配置项。比如日志级别、超时时间、功能开关这类不是密钥的配置用.env文件管理完全没有问题而真正的数据库密码、支付密钥、云服务凭证就应该走平台的密钥通道。把敏感密钥和普通配置分开管理既是安全实践也让运维排查问题时的排障路径更清晰。6. 实测排坑最常见的六个问题与排查思路6.1 读取到 None 但 .env 里明明有值这个问题的出现频率排在所有 dotenv 相关问题里的第一位。以前面说的路径问题为主因但也有几种变体一是load_dotenv()调用在os.getenv()之后顺序反了自然读不到二是文件确实存在但文件名带了隐藏字符比如不小心把文件命名成了.env.txt三是代码在虚拟环境中运行虚拟环境的系统变量把.env里的值覆盖了而默认不覆盖策略让.env里的值输给了系统变量。排查时我一般按这个顺序来先打印os.environ里的目标变量确认是否为None再用find_dotenv()检查能不能找到文件最后看加载结果——load_dotenv()是有返回值的成功加载返回True没找到文件返回False。这个返回值是个很好的探针import os from dotenv import load_dotenv loaded load_dotenv() print(dotenv loaded:, loaded) print(getenv:, os.getenv(API_KEY))如果loaded是False基本可以断定是路径问题参考前面 2.3 节的显式路径方案解决。6.2 引号和特殊字符被解析成奇怪的样子另一次排查经历是配置文件里写了一个很长的密码密码本身带有#和引号结果程序读到的密码和预期不一致认证一直报错。原因是 dotenv 的解析规则#开头表示注释值中的#虽然不会把整行变成注释但在一些版本和配置写法下容易出现截断或解析歧义引号则会被自动移除。最稳妥的做法是凡是包含特殊字符的值一律用双引号包起来并且避免在值里使用$和反引号它们在某些解析场景下会被当作特殊处理。如果你的密码里不可避免地出现了引号字符本身建议考虑用编码后的形式存储或者在系统环境变量中直接设置而不是放进.env文件。6.3 变量里引用其他变量dotenv 支持简单的变量展开。比如你在.env里写了BASE_URLhttp://example.com/api FULL_URL${BASE_URL}/v1解析之后FULL_URL的值会是http://example.com/api/v1。这在拼接 URL、文件路径时很实用但要注意循环引用的问题如果A引用了BB又引用了A解析结果会变成一个空值或异常。我一般只在层级简单、方向明确时才用变量展开复杂的拼接逻辑更倾向于在代码里用字符串格式化完成因为配置文件的职责是提供元数据而不是实现逻辑。6.4 中文和其他非 ASCII 字符乱码.env文件里的值如果包含中文部分环境下读取出来会出现乱码。这通常是因为默认编码不是 UTF-8。解决方案是在调用时显式指定编码load_dotenv(encodingutf-8)同时保证你的.env文件确实以 UTF-8 编码保存。很多编辑器默认保存为 UTF-8 没问题但 Windows 环境下有些编辑器会写入带 BOM 的 UTF-8BOM 头会让第一行变量的键名带上不可见字符进而读取失败。这种情况的处理方式是把文件另存为无 BOM 的 UTF-8或者干脆统一用代码里的encodingutf-8加系统对 BOM 的兼容处理。6.5 override 参数导致线上环境被本地文件覆盖这是一个典型的好心办坏事案例。某次排查线上问题时发现服务读取的配置不是运维平台注入的值而是代码仓库里某份文件的值原因就是有人为了方便本地调试在代码里写了一行load_dotenv(overrideTrue)交付时忘了去掉。结果生产环境启动时这份文件只要存在就会把平台设置的系统变量全部覆盖掉。这类问题的教训是overrideTrue只应该出现在本地开发的临时脚本里绝对不应该出现在正式应用的启动路径中。如果你需要区分本地和线上可以用环境标志来控制是否启用 overrideimport os from dotenv import load_dotenv APP_ENV os.getenv(APP_ENV, development) load_dotenv(override(APP_ENV development))这样即使代码被部署到生产环境只要APP_ENV不是developmentoverride 就不会生效。6.6 配置项类型问题DEBUG 变量不是布尔值最后一个坑来自类型而非加载本身。.env文件里的所有值本质上都是字符串也就是说DEBUGfalse在os.getenv(DEBUG)后得到的是字符串false而不是布尔值False。如果你直接写if os.getenv(DEBUG):那么false这个非空字符串会被判定为真功能开关永远处于打开状态。正确处理方式是把字符串转换成真正的布尔值再判断DEBUG os.getenv(DEBUG, false).lower() in (1, true, yes)同理数字型配置也要记得转换。PORT8080读出来是字符串如果你要把它传给需要整数参数的函数必须先int()。这类问题不会让程序报错只会让程序跑出与预期相悖的结果排查起来往往比显式异常更费时间。经历过几轮项目切换和环境迁移之后我现在对 dotenv 的使用已经形成了肌肉记忆项目里永远有.env.example作为配置清单正式的启动代码里永远显式指定.env路径overrideTrue永远只出现在本地临时脚本中。它不复杂但恰恰是这些不复杂的小决策决定了你的配置体系在关键时刻是帮你快速定位问题、还是让你在排查时无从下手。如果你也在用 dotenv希望你从这篇文章里带走的不仅是 API 调用方式还有那份对配置边界和安全底线的敏感度。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑