一. 前言:秘密不要寫死,設定也不要散落各地 #
拍拍君先講結論:如果你的 Python 專案有 API key、資料庫網址、外部服務 token、部署環境差異,請不要把它們直接寫進程式碼。 你可能看過這種寫法:
OPENAI_API_KEY = "sk-..."
DATABASE_URL = "postgresql://user:password@localhost:5432/app"
DEBUG = True
這樣跑起來很快,但也很快就會出事。 第一個問題是安全:程式碼通常會進 Git,一旦 secret 被 commit,就算你之後刪掉,歷史紀錄裡還是可能找得到。 第二個問題是環境差異:本機、測試機、正式機的設定通常不一樣,寫死在程式裡會讓部署變成手動改 code。 第三個問題是協作:同事 clone 專案後,不知道要設定哪些環境變數,也不知道哪些值是必要的。 python-dotenv 解決的是其中一個很實用的痛點:
讓本機開發可以從
.env檔讀取環境變數,同時讓程式在部署環境仍然使用真正的 environment variables。
也就是說,.env 是本機開發的方便入口,不是正式部署的秘密保險箱。 這篇會帶你把 python-dotenv 用得穩一點:從安裝、載入、override 規則、.env.example、簡單驗證,到部署時要注意的安全細節。 如果你想要更進階的「型別安全設定物件」,可以接著看 pydantic-settings 實戰。 這篇先專心處理最基礎、最常用、最容易被搞混的 .env 工作流。
二. 安裝:只需要一個小套件 #
python-dotenv 不是標準庫,需要另外安裝。 用 pip:
pip install python-dotenv
用 uv:
uv add python-dotenv
專案裡最常見的配置是 .env、.env.example、.gitignore 和你的 Python 程式放在同一個專案根目錄。其中:
.env放你本機真正使用的值.env.example放範例 key,不放真正 secret.gitignore必須排除.envapp.py用load_dotenv()載入
最基本的 .gitignore:
.env
.env.*
!.env.example
這段的意思是:忽略 .env 和 .env.local 之類的本機檔案,但保留 .env.example。 拍拍君建議一開始就這樣設,不要等 token 進 Git 之後才補救。
三. 第一個 .env:把值放到專案外層設定
#
先建立 .env:
APP_NAME=Daily Pypy Lab
DEBUG=true
API_BASE_URL=https://api.example.com
API_KEY=replace-me-with-real-secret
然後在 Python 裡載入:
from dotenv import load_dotenv
import os
load_dotenv()
app_name = os.getenv("APP_NAME")
debug = os.getenv("DEBUG")
api_base_url = os.getenv("API_BASE_URL")
api_key = os.getenv("API_KEY")
print(app_name)
print(debug)
print(api_base_url)
print(api_key)
load_dotenv() 會讀取目前工作目錄附近的 .env 檔,把裡面的 key/value 放進 os.environ。之後你就可以用 os.getenv("API_KEY") 或 os.environ["API_KEY"] 讀取。兩者差異很重要:os.getenv() 找不到時會回傳 None,也可以給預設值:
port = os.getenv("PORT", "8000")
os.environ["API_KEY"] 找不到時會直接丟 KeyError。 拍拍君的習慣是:
- 必填 secret 用
os.environ["KEY"] - 可選設定用
os.getenv("KEY", default) - 啟動時集中檢查,不要散落在每個函式裡
例如:
from dotenv import load_dotenv
import os
load_dotenv()
API_KEY = os.environ["API_KEY"]
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
DEBUG = os.getenv("DEBUG", "false")
這樣少一個必要設定時,程式會在啟動階段失敗,而不是跑到一半才爆炸。
四. .env 格式:簡單,但有幾個細節
#
.env 基本上是一行一個 key/value:
API_KEY=abc123
PORT=8000
DEBUG=true
可以加註解,也可以替含空白的值加引號:
# local development only
APP_NAME="Daily Pypy Lab"
WELCOME_MESSAGE="hello, dotenv"
PASSWORD="abc#123"
如果值裡有空白或 #,建議加引號,避免被當成註解或被解析錯。 .env 裡的值都是字串。 所以這段:
DEBUG=false
PORT=8000
讀出來會是字串,不是 boolean 或 int:
debug = os.getenv("DEBUG")
port = os.getenv("PORT")
print(type(debug), debug)
print(type(port), port)
如果你需要型別轉換,要自己處理:
debug = os.getenv("DEBUG", "false").lower() == "true"
port = int(os.getenv("PORT", "8000"))
簡單專案這樣可以。 設定很多、規則變複雜時,就適合改用 pydantic-settings 這類工具。
五. override 規則:為什麼我的 .env 沒有生效?
#
python-dotenv 最常見的困惑是:
我明明改了
.env,為什麼程式讀到的值還是舊的?
關鍵在 load_dotenv() 預設不會覆蓋已經存在的環境變數。 例如你的 shell 已經有:
export API_KEY=from-shell
.env 裡有:
API_KEY=from-dotenv
Python 程式:
from dotenv import load_dotenv
import os
load_dotenv()
print(os.getenv("API_KEY"))
輸出會是:
from-shell
這是好事,因為正式部署平台通常會把真正的環境變數注入 process,而 .env 只該是本機開發輔助。 如果你真的需要讓 .env 覆蓋目前 shell 裡的值,可以明確寫:
load_dotenv(override=True)
但拍拍君建議 production entrypoint 不要隨手加 override=True。 比較好的做法是把意圖寫清楚:
load_dotenv(override=False)
就算這是預設值,也能讓讀 code 的人知道:shell / deployment environment 優先,.env 只是補缺。
六. 指定 .env 路徑:不要被工作目錄騙
#
load_dotenv() 會嘗試尋找 .env,小專案通常沒問題。 但一旦你有 CLI、背景 worker、測試、Docker、不同啟動位置,就可能遇到工作目錄不一致。 比較穩的寫法是用 pathlib 指定路徑:
from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent
ENV_PATH = BASE_DIR / ".env"
load_dotenv(ENV_PATH, override=False)
如果你的程式在 src/ 裡,而 .env 在專案根目錄,可以往上找:
from pathlib import Path
from dotenv import load_dotenv
ROOT_DIR = Path(__file__).resolve().parents[2]
load_dotenv(ROOT_DIR / ".env", override=False)
parents[2] 這種寫法要小心,目錄結構改了就會壞。 所以拍拍君更喜歡把設定載入集中在一個地方,例如 src/my_app/config.py,不要每個模組都自己找 .env。
七. 建立 .env.example:讓協作變簡單
#
.env 不該進 Git,但專案需要告訴別人「有哪些設定要填」。 這就是 .env.example 的工作。 例如:
APP_NAME=Daily Pypy Lab
DEBUG=false
API_BASE_URL=https://api.example.com
API_KEY=
DATABASE_URL=
重點是:
- key 要完整
- secret 值要留空或放假資料
- 預設值要保守
- 註解要說明必要格式
README 裡就可以寫 cp .env.example .env,然後提醒開發者填自己的值。這比在 README 裡列一大串環境變數更不容易漏。
八. 集中設定:不要在程式各處直接讀環境變數 #
很多專案一開始會在不同檔案到處呼叫 os.getenv()。 短期看起來方便,長期會讓設定規則散落各地。 拍拍君建議建立一個簡單的 config.py,集中載入、轉型和必填檢查:
from pathlib import Path
from dotenv import load_dotenv
import os
ROOT_DIR = Path(__file__).resolve().parents[1]
load_dotenv(ROOT_DIR / ".env", override=False)
def require_env(name: str) -> str:
value = os.getenv(name)
if value is None or value.strip() == "":
raise RuntimeError(f"Missing required environment variable: {name}")
return value
APP_NAME = os.getenv("APP_NAME", "my-app")
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
API_KEY = require_env("API_KEY")
DATABASE_URL = require_env("DATABASE_URL")
其他地方只 import 這些設定。 這樣有幾個好處:
- 必填設定集中在一個地方
- 型別轉換集中在一個地方
- 測試時比較容易替換
- 不會每個檔案都呼叫
load_dotenv()
python-dotenv 本身很輕量,不會幫你做完整設定系統。 但只要你把讀取規則集中起來,小型專案通常就夠穩。
九. 做一點必要驗證:早點失敗比較好 #
環境變數最怕的是「看似有值,但其實不能用」。 例如:
API_KEY=replace-me
DATABASE_URL=
PORT=not-a-number
程式如果不檢查,可能啟動成功,直到真的呼叫 API 或連 DB 時才失敗。 最少可以做三件事:
- 必填值空白就立刻失敗
- boolean 明確接受
true/false或1/0 - port、timeout 這類數字要轉成
int
簡化版可以長這樣:
from dotenv import load_dotenv
import os
load_dotenv(override=False)
API_KEY = os.environ["API_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
DEBUG = os.getenv("DEBUG", "false").lower() in {"1", "true", "yes"}
PORT = int(os.getenv("PORT", "8000"))
這不是最華麗的設定系統,但對小專案很實用。 重點是錯誤要清楚。 不要讓使用者只看到 KeyError: 'API_KEY',如果可以,讓錯誤訊息變成 Missing required environment variable: API_KEY。
十. CLI、FastAPI、測試裡怎麼放? #
不同專案型態,load_dotenv() 放的位置略有不同。
- CLI 工具:放在 main entrypoint 或
config.py - FastAPI:放在建立 app 前,或讓 app import
config.py - background worker:放在 worker 啟動入口
- pytest:測試中用
monkeypatch明確設定環境變數
測試不一定要真的讀 .env。很多時候直接用 monkeypatch.setenv() 會更穩,因為測試資料就寫在測試裡,不依賴開發者本機檔案。
十一. 部署:正式環境不要依賴 .env 檔
#
本機開發用 .env 很舒服。 正式部署時,通常應該使用平台提供的 environment variables 或 secrets manager。 例如:
- GitHub Actions:Repository secrets / environment secrets
- Docker Compose:
env_file或environment - Kubernetes:Secret / ConfigMap
- Render、Railway、Fly.io、Vercel:平台後台設定
GitHub Actions 裡不要把 secret 寫進 workflow,改用 ${{ secrets.API_KEY }}:
jobs:
test:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Docker Compose 則要分清楚 .env 和 env_file:
services:
app:
build: .
env_file:
- .env
environment:
DEBUG: "false"
Compose 專案根目錄的 .env 常用來替換 compose.yml 裡的變數;env_file 則是把檔案裡的 key/value 傳進 container 環境。 如果你不確定,請明確寫 env_file,並用 docker compose config 檢查最後產生的設定。 部署重點是:.env 可以用於本機,正式 secret 應該由平台注入,程式仍然用 os.environ 讀,不需要知道 secret 來自哪裡。
十二. 安全清單:上線前檢查一次 #
.env 最大的風險不是工具本身,而是使用習慣。 拍拍君的上線前清單:
.env有在.gitignore.env.example有 commit.env.example沒有真 secret- README 有寫
cp .env.example .env - 必填變數啟動時會檢查
- 程式不會把 secret 印到 log
- CI/CD secret 放在平台設定,不寫進 workflow
- production 不依賴開發機
.env - token 不小心 commit 過就直接 rotate
如果你懷疑 secret 已經進過 Git 歷史,不要只刪掉那一行。 請直接去 provider 後台撤銷或重產 token。 Git 歷史清理很麻煩,而且你無法保證遠端、fork、CI log、快取裡沒有留下痕跡。 也不要在錯誤訊息或 debug log 裡印完整 os.environ;secret 一旦進 log,log 平台、告警系統、截圖都可能變成洩漏來源。
結語:dotenv 是入口,不是整套安全系統 #
python-dotenv 很適合解決本機開發的設定問題。 它讓你可以把 secret 和環境差異移出程式碼,用 .env 管理本機值,再用平台環境變數處理正式部署。 但它不是 secret manager,也不是完整設定框架。 拍拍君建議你記住三個原則: 第一,.env 不進 Git,.env.example 才進 Git。 第二,production 環境變數優先,不要隨便 override=True。 第三,啟動時集中檢查設定,早點失敗比晚點爆炸好。 把這些習慣養起來,你的 Python 專案會少很多「本機可以,部署不行」的謎之問題。 設定這種東西平常很安靜,一出事就很吵。 所以我們先把它收好。拍拍。