برای جلوگیری از قرار دادن کلیدها و رمزها داخل کد، از فایل .env و کتابخانه python-dotenv در پایتون استفاده کنید: فایل .env را بسازید، آن را به .gitignore اضافه کنید، سپس با load_dotenv بارگذاری کنید تا مقادیر به os.environ منتقل شوند و کلیدها در مخزن کد قرار نگیرند.

مسیر سریع اجرا (Quickstart)

  1. نصب بسته:
    pip install python-dotenv
  2. ساخت فایل .env در ریشه پروژه:
    # .env
    DEBUG=true
    API_KEY=your_real_api_key_here
    DB_HOST=localhost
    DB_PORT=5432
    
  3. افزودن به .gitignore:
    .env
    .env.*
    
  4. بارگذاری در ابتدای entrypoint برنامه:
    import os
    from dotenv import load_dotenv, find_dotenv
    
    # محیط واقعی (مثلا CI/سرور) باید بر .env اولویت داشته باشد
    load_dotenv(find_dotenv(), override=False)
    
    API_KEY = os.getenv("API_KEY")
    if not API_KEY:
        raise RuntimeError("API_KEY is missing")
    
    DEBUG = os.getenv("DEBUG", "false").lower() in {"1", "true", "yes", "on"}
    print("DEBUG:", DEBUG)
  5. اجرا و بررسی: اگر DEBUG چاپ شد و در نبود API_KEY خطا گرفتید، پیکربندی درست است.

الگوی پیشنهادی برای توسعه و تولید

به طور معمول .env برای توسعه محلی و تست استفاده می‌شود. در محیط تولید، مقداردهی از طریق متغیرهای محیطی سیستم، سرویس ابری یا ارکستریتور انجام می‌گیرد. با override=False، اگر در تولید متغیرها از قبل ست شده باشند، مقادیر .env جایگزینشان نمی‌شوند. در محیط توسعه می‌توانید override را روشن کنید تا فایل .env مقدم باشد.

یک مثال عملی: تنظیمات امن با dataclass

در مثال زیر ساختار خوانا و قابل اعتبارسنجی برای تنظیمات می‌سازیم. اگر با dataclass آشنا نیستید، راهنمای کامل آن در کدیتی موجود است: آموزش dataclass در پایتون برای مدل‌های داده تمیز.

from dataclasses import dataclass
from typing import Iterable
import os
from dotenv import load_dotenv, find_dotenv

# بارگذاری زودهنگام
load_dotenv(find_dotenv(usecwd=True), override=False, encoding="utf-8")

def require(name: str) -> str:
    value = os.getenv(name)
    if not value:
        raise ValueError(f"{name} is required")
    return value

def to_bool(value: str, default: bool = False) -> bool:
    if value is None:
        return default
    return str(value).strip().lower() in {"1", "true", "yes", "on", "y"}

def to_int(value: str, default: int) -> int:
    try:
        return int(value) if value is not None else default
    except ValueError:
        raise ValueError(f"Expected integer, got: {value!r}")

@dataclass(frozen=True)
class Settings:
    api_key: str
    db_host: str
    db_port: int
    debug: bool

    @classmethod
    def from_env(cls) -> "Settings":
        return cls(
            api_key=require("API_KEY"),
            db_host=os.getenv("DB_HOST", "localhost"),
            db_port=to_int(os.getenv("DB_PORT"), 5432),
            debug=to_bool(os.getenv("DEBUG"), default=False),
        )

def validate_required(names: Iterable[str]) -> None:
    missing = [n for n in names if not os.getenv(n)]
    if missing:
        raise RuntimeError(f"Missing required envs: {', '.join(missing)}")

if __name__ == "__main__":
    validate_required(["API_KEY"])
    settings = Settings.from_env()
    print("DB:", settings.db_host, settings.db_port)

روش بررسی: با خالی کردن API_KEY باید خطا ببینید. با مقداردهی DB_PORT غیرعددی باید خطای معتبر دریافت کنید. این رفتارها نشان می‌دهند اعتبارسنجی شما کار می‌کند.

ساخت DSN، توسعه محلی و تولید

گاهی از چند متغیر برای ساخت یک آدرس اتصال (DSN) استفاده می‌شود. python-dotenv از جایگذاری متغیرها پشتیبانی می‌کند.

# .env
DB_USER=app
DB_PASS=s3cret
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}
from dotenv import load_dotenv
import os

load_dotenv()
dsn = os.getenv("DATABASE_URL")
if not dsn:
    raise RuntimeError("DATABASE_URL is missing")
print(dsn)

در تولید، توصیه می‌شود DATABASE_URL را مستقیما از متغیرهای محیطی واقعی تأمین کنید تا وابسته به فایل .env نباشید.

dotenv_values یا load_dotenv؟

load_dotenv مقادیر را وارد os.environ می‌کند و تمام کدهای استاندارد پایتون (و کتابخانه‌ها) می‌توانند به آن دسترسی داشته باشند. در مقابل، dotenv_values فقط مقادیر را به صورت دیکشنری برمی‌گرداند و محیط فرآیند را تغییر نمی‌دهد:

from dotenv import dotenv_values

cfg = dotenv_values()  # {'API_KEY': '...'}
print(cfg.get("API_KEY"))

اگر با کتابخانه‌های دیگری کار می‌کنید که تنظیمات را از os.environ می‌خوانند (مانند درایورهای پایگاه داده یا فریمورک‌ها)، از load_dotenv استفاده کنید.

چند محیط (.env.local، .env.test و …)

می‌توانید برای محیط‌های مختلف فایل‌های جداگانه داشته باشید و به ترتیب بارگذاری کنید. ترتیب را شفاف و کنترل شده نگه دارید:

from pathlib import Path
from dotenv import load_dotenv

root = Path(__file__).resolve().parent
# پایه
load_dotenv(root / ".env", override=False)
# اختصاصی دستگاه توسعه (نادیده‌گرفته‌شده در گیت)
load_dotenv(root / ".env.local", override=True)
# تست خودکار
load_dotenv(root / ".env.test", override=True)

قاعده ساده: محیط واقعی (متغیرهای OS/CI) باید در نهایت قابل غلبه توسط فایل‌ها نباشند؛ بنابراین برای تولید معمولا همان بارگذاری پایه با override=False کافی است یا کلا بارگذاری را غیرفعال می‌کنید.

نکات امنیتی و خطاهای رایج

  • .env را هرگز در مخزن گیت قرار ندهید. الگوهای .gitignore بالا را اضافه کنید.
  • از چاپ مستقیم رمزها در لاگ بپرهیزید. اگر نیاز به عیب‌یابی است، مقدار را ماسک کنید:
    def mask(s: str, show: int = 4) -> str:
        if not s:
            return ""
        return s[:show] + "..." + ("*" * max(0, len(s) - show - 3))
    
    print("API_KEY:", mask(os.getenv("API_KEY", "")))

    برای لاگ نویسی حرفه‌ای‌تر می‌توانید از Loguru استفاده کنید: کتابخانه Loguru؛ راهنمای کامل لاگ نویسی در پایتون.

  • اگر کلید چندخطی (مثل گواهی) دارید، آن را در .env با کاراکترهای \n ذخیره کنید و هنگام خواندن جایگزین کنید:
    PRIVATE_KEY = os.getenv("PRIVATE_KEY", "").replace("\\n", "\n")
  • فاصله‌های اضافی و کوتیشن‌ها را مدیریت کنید. قالب استاندارد KEY=value است. برای مقادیر شامل فاصله از کوتیشن استفاده کنید: TITLE="My App Dev".
  • مقداردهی نوعی را جدی بگیرید: بولین و عدد را به درستی تبدیل کنید تا خطاهای منطقی رخ ندهد.

تست پیکربندی بدون افشای رمز

در تست‌ها متغیرهای محیطی را تزریق کنید؛ به .env وابسته نشوید.

# test_settings.py
from myapp.config import Settings

def test_settings_from_env(monkeypatch):
    monkeypatch.setenv("API_KEY", "testkey")
    monkeypatch.setenv("DB_PORT", "1234")
    s = Settings.from_env()
    assert s.api_key == "testkey"
    assert s.db_port == 1234

این کار تست‌ها را مستقل و قابل تکرار می‌کند.

Docker و Kubernetes چه؟

python-dotenv برای توسعه محلی عالی است؛ اما در کانتینرها بهتر است رمزها را با مکانیسم‌های بومی تزریق کنید (Docker secrets، Kubernetes Secrets یا متغیرهای محیطی استقرار). برای دیدن الگوهای امن در این فضا به راهنمای مدیریت Secret در Docker مراجعه کنید.

پیکربندی CLI یا ابزارهای کوچک

در ابزارهای خط فرمان که با ورودی‌های مختلف اجرا می‌شوند، بارگذاری اولیه و اعتبارسنجی سریع اهمیت دارد. اگر قصد ساخت CLI دارید، آشنایی با Typer مفید است: آموزش Typer در پایتون: ساخت ابزار خط فرمان حرفه‌ای.

عیب‌یابی سریع

  • load_dotenv را خیلی دیر صدا زده‌اید؟ آن را در ابتدای entrypoint اجرا کنید، قبل از importهایی که env می‌خوانند.
  • فایل .env پیدا نمی‌شود؟ از find_dotenv(usecwd=True) یا مسیر صریح با Path استفاده کنید.
  • مقادیر .env اعمال نمی‌شوند؟ بررسی کنید override=False باشد و آیا متغیرها از قبل در محیط ست شده‌اند.
  • کاراکترهای غیرلاتین به‌هم‌ریخته‌اند؟ پارامتر encoding=”utf-8″ را به load_dotenv بدهید.

گام بعدی

الگوی بالا را در پروژه اعمال کنید: .env را بسازید، .gitignore را به‌روز کنید، بارگذاری زودهنگام و اعتبارسنجی داشته باشید، و در تولید از متغیرهای واقعی محیط یا Secret Manager استفاده کنید. با همین گام‌های ساده، ریسک افشای کلیدها به طور محسوسی کاهش می‌یابد و پیکربندی پروژه شما قابل اتکا می‌شود.