حفظ مفاتيح API في بايثون باستخدام .env وpython-dotenv

حفظ API Key خارج الكود وقراءة متغيرات البيئة في بايثون باستخدام python-dotenv

قد يعمل برنامج بايثون بشكل صحيح على جهازك، ثم يتوقف فجأة عندما تنقله إلى مشروع آخر لأن مفتاح API غير موجود أو لأن إعدادات التشغيل مختلفة. إذا كنت تضع المفاتيح داخل ملفات .py مباشرة، فستحتاج إلى تعديل الكود عند تغيير الإعدادات، وقد تنشر سرًا عن طريق الخطأ.

في هذا الدرس سنشرح حفظ مفاتيح API في بايثون باستخدام ملف .env ومكتبة python-dotenv، بدءًا من أول مثال يعمل، مرورًا بحل مشكلة os.getenv() التي تعيد None، وحتى مشروع صغير يقرأ إعداداته ويتحقق منها دون إظهار قيمة المفتاح في الطرفية.

{alertInfo}المتطلبات: Python 3.10 أو أحدث ومكتبة python-dotenv. لا تحتاج إلى إنشاء مفتاح API حقيقي أو الاتصال بأي خدمة مدفوعة لتجربة الدرس. كل القيم المستخدمة في الأمثلة تجريبية.

{getToc} $title={محتوى المقال}


ما هو ملف .env في بايثون؟ ولماذا نستخدمه؟

ملف .env ملف نصي عادي تُكتب فيه الإعدادات عادة بصيغة KEY=VALUE. على سبيل المثال، قد يحتوي اسم التطبيق أو المنفذ الذي يعمل عليه أو مفتاح وصول إلى خدمة خارجية. يُبقي ذلك الإعدادات منفصلة عن كود البرنامج؛ فيمكن تغييرها دون تعديل ملفات بايثون نفسها.

APP_NAME=Arab Python Demo
PORT=8080
DEBUG=false
API_KEY=demo_key_replace_me

تقرأ مكتبة python-dotenv القيم من الملف وتضيفها إلى متغيرات بيئة العملية الحالية عند استدعاء load_dotenv()، ويمكن حينها قراءتها عبر os.getenv() أو os.environ. المكتبة لا تشفّر القيم ولا تُخفي الملف عمّن يستطيع قراءته على الجهاز.

{alertWarning}تنبيه أمني: إبعاد مفتاح API عن ملف الكود يقلّل احتمالات نشره بالخطأ، لكنه لا يحوّل .env إلى خزنة أسرار. في بيئة الإنتاج استخدم متغيرات البيئة أو خدمة إدارة أسرار مناسبة، ولا ترفع الملف الذي يحتوي قيمًا حقيقية إلى المستودع.

ما الفرق بين .env و .venv في بايثون؟

التشابه بين الاسمين سبب متكرر للالتباس في نتائج البحث، لكن وظيفتهما مختلفة تمامًا:

العنصرماذا يحتوي؟وظيفته
.envقيم نصية مثل API_KEY=...حفظ إعدادات المشروع خارج ملفات الكود
.venv/مفسّر Python ومكتبات بيئة افتراضيةفصل الحزم والتبعيات عن بقية المشاريع
متغير البيئةقيمة داخل بيئة عملية التشغيليقرؤها التطبيق عبر os.getenv()

يمكن لمشروع واحد استخدام .venv لتثبيت المكتبات و.env للإعدادات في الوقت نفسه. للمزيد عن البيئات الافتراضية، راجع تجهيز بيئة بايثون للمشاريع في موقع بايثون العرب.

مقارنة توضيحية بين ملف الإعدادات ومتطلبات البيئة الافتراضية

تجهيز مشروع بايثون وملف .env

أنشئ مجلدًا باسم python-env-demo، ثم ضع داخله الملفات التالية. انتبه إلى أن اسم ملف الإعدادات يبدأ بنقطة وأن اسمه الكامل .env، وليس .env.txt.

python-env-demo/
├── .env
└── first_env.py

داخل .env، ضع بيانات تجريبية لا قيمة أمنية لها:

APP_NAME=Arab Python Demo
API_KEY=demo_key_replace_me
PORT=8080
DEBUG=false

لا يلزم استخدام علامات اقتباس حول الأرقام أو النصوص البسيطة. وإذا احتوت القيمة على مسافات أو رموز خاصة، فالأفضل كتابتها بين علامتي اقتباس مزدوجتين. احفظ الملف بترميز UTF-8.

تثبيت مكتبة python-dotenv

نفّذ في الطرفية، داخل بيئة بايثون التي ستشغّل بها المشروع:

python -m pip install python-dotenv

للتحقق من التثبيت:

python -m pip show python-dotenv

اسم الحزمة أثناء التثبيت هو python-dotenv، لكن اسم الاستيراد في الكود هو dotenv، وليس python_dotenv. إذا ظهر الخطأ ModuleNotFoundError: No module named dotenv فغالبًا ثُبّتت المكتبة في بيئة أو مفسّر مختلف عن الذي يشغّل البرنامج.

أول كود: قراءة متغير من ملف .env باستخدام load_dotenv

أنشئ ملف first_env.py في المجلد نفسه، ثم انسخ المثال التالي:

import os
from pathlib import Path
from dotenv import load_dotenv

env_file = Path(__file__).resolve().parent / ".env"
load_dotenv(dotenv_path=env_file)

app_name = os.getenv("APP_NAME", "Python Demo")
api_key = os.getenv("API_KEY")

print("اسم المشروع:", app_name)
print("هل تم تحميل المفتاح؟", bool(api_key))

شغّل الملف باستخدام python first_env.py. مع ملف الإعدادات التجريبي السابق، ستكون النتيجة:

اسم المشروع: Arab Python Demo
هل تم تحميل المفتاح؟ True

السطر Path(__file__).resolve().parent يجعل مسار ملف الإعدادات معتمدًا على مكان السكربت، لا على المجلد الذي فتحت منه الطرفية. أما os.getenv("APP_NAME", "Python Demo") فيستخدم القيمة الاحتياطية فقط إذا لم يجد المتغير. لم نطبع قيمة API_KEY؛ تحققنا من وجودها فحسب.

مسار انتقال البيانات من .env إلى load_dotenv ثم os.getenv دون عرض المفتاح

كيف تحفظ مفتاح API خارج الكود دون كشفه؟

عند التعامل مع بوت أو خدمة API، قد تحتاج إلى كتابة رمز وصول فعلي. في المشروع المحلي ضع المفتاح في ملف .env غير المتتبَّع بواسطة Git، واقرأه داخل البرنامج عند التشغيل. لا تستخدم مفتاحًا حقيقيًا عند تجربة هذا الدرس؛ القيمة demo_key_replace_me ليست مفتاحًا يعمل مع أي خدمة.

from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv(Path(__file__).resolve().parent / ".env")

api_key = os.getenv("API_KEY", "").strip()
if not api_key:
    raise RuntimeError("API_KEY غير موجودة في الإعدادات")

# استخدم api_key عند استدعاء API فعلية في مشروعك.
print("تم تجهيز مفتاح API للاستخدام (القيمة مخفية)")

بهذه الطريقة يبقى الكود قابلًا للمشاركة دون إدراج قيمة المفتاح فيه. لكن إخفاء المفتاح في مخرجات البرنامج لا يعفيك من حماية الملف نفسه ومنع تسريبه في السجلات ولقطات الشاشة وملفات النسخ الاحتياطي العامة.

تحويل القيم النصية إلى أرقام وTrue أو False

تُقرأ متغيرات البيئة باعتبارها نصوصًا. حتى لو كتبت PORT=8080، فإن os.getenv("PORT") يعيد النص "8080"، وليس العدد الصحيح 8080. وبالمثل، فإن bool("false") في بايثون تُعيد True لأن النص غير فارغ؛ ولهذا يجب تحويل القيم بشكل صريح.

import os
from dotenv import load_dotenv

load_dotenv()

port = int(os.getenv("PORT", "8080"))
debug_text = os.getenv("DEBUG", "false").strip().lower()
if debug_text not in {"true", "false"}:
    raise ValueError("DEBUG يجب أن تكون true أو false")
debug = debug_text == "true"

print(type(port).__name__, port)
print(type(debug).__name__, debug)
int 8080
bool False

لبرنامج حقيقي، ينبغي أيضًا التحقق من أن المنفذ بين 1 و65535، والتعامل مع إدخال نص غير رقمي بدل السماح بظهور ValueError غير مفهوم. ستجد هذه المعالجة جاهزة في ملف config.py داخل المشروع المرفق.

حل مشكلة os.getenv() ترجع None أو load_dotenv لا تعمل

إذا كان الملف موجودًا لكن os.getenv("API_KEY") تعيد None، فلا تفترض أن المكتبة معطلة. افحص المسار واسم المتغير ووقت تحميل الملف أولًا. قد يحدث أيضًا أن يُكتب API_KEY دون علامة =، أو أن تُقرأ القيمة من ملف آخر غير المقصود.

from pathlib import Path
from dotenv import dotenv_values

file_path = Path(__file__).resolve().parent / ".env"
print("هل ملف .env موجود؟", file_path.is_file())

if file_path.is_file():
    values = dotenv_values(file_path)
    print("هل اسم API_KEY موجود؟", "API_KEY" in values)
    print("هل القيمة غير فارغة؟", bool(values.get("API_KEY")))

هذا الفحص لا يطبع قيمة المفتاح نفسها. شغّله من داخل مشروعك، ثم راجع الأسباب الأكثر شيوعًا بالترتيب:

  1. تأكد من كتابة .env بالاسم الصحيح، ومن ظهور الامتداد الحقيقي للملف في Windows.
  2. تحقق من وجوده بجانب السكربت أو حدد مساره الكامل باستخدام Path(__file__).
  3. استدعِ load_dotenv() قبل قراءة المتغير بواسطة os.getenv().
  4. استخدم اسم المتغير نفسه حرفيًا، مثل API_KEY وليس APIKEY.
  5. تأكد من أن القيمة ليست فارغة، وأنك لم تضبط متغير نظام آخر يسبقها في الأولوية.
{alertInfo}أحيانًا يكون المتغير موجودًا في بيئة التشغيل بقيمة مختلفة أو فارغة؛ لأن load_dotenv() لا يستبدل المتغير الموجود افتراضيًا. سيشرح القسم التالي هذا السلوك.

مخطط تشخيص وجود ملف .env وصحة اسمه ومساره وتوقيت تحميله

أولوية المتغيرات: ما الفرق بين override=False و override=True؟

إذا كان APP_NAME موجودًا في نظام التشغيل وداخل .env معًا، فأيهما يُستخدم؟ افتراضيًا تمنح load_dotenv() الأولوية لمتغير البيئة الموجود بالفعل، لأن قيمة override تساوي False. أما override=True فيسمح لقيمة الملف باستبداله داخل العملية.

import os
from pathlib import Path
from dotenv import load_dotenv

file_path = Path(__file__).resolve().parent / ".env"
os.environ["APP_NAME"] = "From System"

load_dotenv(file_path, override=False)
print(os.getenv("APP_NAME"))

load_dotenv(file_path, override=True)
print(os.getenv("APP_NAME"))

باستخدام APP_NAME=Arab Python Demo في ملفنا التجريبي، تكون النتيجة:

From System
Arab Python Demo

الأفضل غالبًا ترك override=False حتى تستطيع منصات النشر حقن إعداداتها عبر متغيرات البيئة، بدل إجبار التطبيق على استخدام ملف محلي. المثال السابق تعليمي لتوضيح الفرق، وليس دعوة لاستبدال إعدادات الإنتاج عشوائيًا.

ما الفرق بين load_dotenv و dotenv_values؟

تعمل load_dotenv() على تحميل القيم داخل os.environ في عملية بايثون الجارية. أما dotenv_values() فتعيد قاموسًا بقيم الملف دون تعديل متغيرات البيئة، وهو أسلوب مفيد إذا أردت فحص الملف أو دمج عدة مصادر للإعدادات بنفسك.

from pathlib import Path
from dotenv import dotenv_values

file_path = Path(__file__).resolve().parent / ".env"
values = dotenv_values(file_path)

print("المفاتيح المتاحة:", sorted(values.keys()))
print("هل يحتوي الملف على API_KEY؟", bool(values.get("API_KEY")))

لا تطبع قاموس values كاملًا إذا احتوى على كلمات مرور أو مفاتيح حقيقية. كذلك، فإن قراءة الملف بواسطة dotenv_values() لا تضمن وحدها أن جميع القيم موجودة في البيئة التي يستخدمها os.getenv().

حماية ملف .env عند رفع المشروع إلى GitHub

لأن .env ملف نصي عادي، يجب منع Git من رفعه إذا كان يحتوي بيانات حساسة. أنشئ ملفًا باسم .gitignore في جذر مشروعك وأضف:

.env
.env.*
!.env.example
.venv/
__pycache__/
*.py[cod]

ثم أنشئ ملفًا باسم .env.example يحتوي أسماء المتغيرات وقيمًا تجريبية فقط؛ بهذا يستطيع من يحصل على المشروع معرفة إعداداته المطلوبة دون الاطلاع على الأسرار:

APP_NAME=Arab Python Demo
API_KEY=demo_key_replace_me
PORT=8080
DEBUG=false

إذا كنت قد أضفت .env إلى Git قبل كتابة .gitignore، فيمكن إيقاف تتبعه مستقبلًا بالأمر التالي بعد حفظ التغييرات المناسبة:

git rm --cached .env
{alertWarning}إذا تسرب مفتاح حقيقي: إيقاف تتبع الملف لا يحذف السر من تاريخ Git أو نسخه السابقة. ألغِ المفتاح أو دوّره فورًا لدى مزود الخدمة، ثم اتبع إرشادات معالجة التسريب. قد تحتاج إلى تنظيف تاريخ المستودع أيضًا، مع الانتباه إلى أثر ذلك على المشاركين.

يمكن الرجوع إلى إرشادات GitHub لمعالجة المفاتيح المتسربة، وإلى التوثيق الرسمي لمكتبة python-dotenv لمزيد من التفاصيل.

مشروع عملي: قراءة الإعدادات والتحقق منها في config.py

بدل كتابة load_dotenv() بشكل متكرر في كل ملف، سنضع تحميل الإعدادات والتحقق منها في وحدة مستقلة اسمها config.py. المشروع المرفق يحوي أيضًا app.py الذي يطبع الإعدادات العامة ويخفي قيمة المفتاح، وcheck_env.py لاكتشاف مشاكل الملف دون إفشاء القيم.

هذا هو الكود الكامل لملف config.py في حزمة المشروع:

"""إدارة إعدادات مشروع بايثون من متغيرات البيئة وملف .env."""

import os
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Union

from dotenv import load_dotenv

PROJECT_DIR = Path(__file__).resolve().parent
ENV_FILE = PROJECT_DIR / ".env"


@dataclass(frozen=True)
class Settings:
    app_name: str
    api_key: str
    port: int
    debug: bool


def parse_bool(value: str) -> bool:
    """تحويل true/false وyes/no و1/0 إلى قيمة منطقية صحيحة."""
    normalized = value.strip().lower()
    if normalized in {"true", "1", "yes", "on"}:
        return True
    if normalized in {"false", "0", "no", "off"}:
        return False
    raise ValueError("DEBUG يجب أن تكون true أو false (أو 1/0).")


def load_settings(
    env_path: Optional[Union[str, Path]] = None,
    override: bool = False,
) -> Settings:
    """تحميل ملف .env الاختياري ثم التحقق من القيم المطلوبة."""
    path = Path(env_path) if env_path is not None else ENV_FILE
    load_dotenv(dotenv_path=path, override=override)

    app_name = os.getenv("APP_NAME", "Python Env Demo").strip()
    if not app_name:
        raise ValueError("APP_NAME لا يمكن أن تكون فارغة.")

    api_key = os.getenv("API_KEY", "").strip()
    if not api_key:
        raise ValueError("API_KEY غير موجودة. أنشئ .env من .env.example أو اضبط متغير النظام.")

    raw_port = os.getenv("PORT", "8080").strip()
    try:
        port = int(raw_port)
    except ValueError as exc:
        raise ValueError("PORT يجب أن تكون رقمًا صحيحًا.") from exc
    if not 1 <= port <= 65535:
        raise ValueError("PORT يجب أن تكون بين 1 و65535.")

    debug = parse_bool(os.getenv("DEBUG", "false"))
    return Settings(app_name=app_name, api_key=api_key, port=port, debug=debug)

تُحوِّل الدالة parse_bool() النص إلى قيمة منطقية مع التحقق من القيم المقبولة، وتتحقق load_settings() من وجود المفتاح ومنفذ التشغيل. لاحظ أنها لا تشترط وجود ملف .env إذا كانت متغيرات النظام مضبوطة أصلًا، وهي ميزة مفيدة على خوادم الإنتاج.

ملف app.py — استعمال الإعدادات

"""عرض إعدادات آمنة دون كشف قيمة مفتاح API."""

from config import load_settings


def main() -> int:
    try:
        settings = load_settings()
    except ValueError as error:
        print(f"خطأ في الإعدادات: {error}")
        return 1

    print(f"اسم المشروع: {settings.app_name}")
    print(f"المنفذ: {settings.port}")
    print(f"وضع التصحيح: {settings.debug}")
    print("مفتاح API: تم تحميله (القيمة مخفية)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

المفتاح يُستخدم هنا للتحقق من توافر إعداد التشغيل فقط، وليس لإرسال طلبات إلى API حقيقية. لذلك يستطيع القارئ تجربة المشروع دون حساب خدمة خارجية. لا تستخدم المفتاح التجريبي في تطبيق منشور.

تشغيل المشروع وقراءة مخرجاته

بعد تحميل الحزمة وفك ضغطها، افتح الطرفية داخل المجلد الذي يحتوي requirements.txt، وثبّت المكتبة:

python -m pip install -r requirements.txt

في Windows PowerShell أنشئ نسخة محلية من ملف الإعدادات:

Copy-Item .env.example .env

وفي Linux أو macOS استخدم:

cp .env.example .env

يمكنك الآن تشغيل المثال والاختبارات:

python app.py
python check_env.py
python -m unittest discover -s tests -v

الناتج الأساسي المتوقع من app.py هو:

اسم المشروع: Arab Python Demo
المنفذ: 8080
وضع التصحيح: False
مفتاح API: تم تحميله (القيمة مخفية)

حزمة الاختبارات تستخدم ملفات إعدادات مؤقتة وقيمًا وهمية؛ وتفحص فقدان المفتاح، والمنفذ غير الصحيح، وأولوية المتغيرات، والتحويل إلى قيم منطقية. إذا غيّرت .env سيختلف الناتج بحسب إعداداتك الحالية، وهذا هو السلوك المقصود. وقد ترى في check_env.py أن متغير النظام غير مضبوط رغم وجود الملف، لأن تلك الأداة تفحصه بواسطة dotenv_values() ولا تستدعي load_dotenv()؛ وهذا اختلاف مقصود بين العمليتين.

الأخطاء الشائعة عند استخدام python-dotenv

المشكلةالسبب المحتملالإجراء المقترح
No module named dotenvالمكتبة لم تثبّت في المفسر الحاليشغّل python -m pip install python-dotenv وتأكد من بيئة التشغيل
os.getenv() == Noneلم يُحمّل المتغير أو اسمه غير صحيحافحص وجود .env والمسار وترتيب load_dotenv()
.env لا يغيّر القيمةمتغير النظام له أولوية مع override=Falseافحص متغير النظام؛ غيّر override فقط إذا كنت تفهم نتيجته
PORT لا تتحول إلى عددالقيمة نص غير صالحتحقق من المحتوى واستخدم int() مع معالجة الخطأ
DEBUG=false تُقرأ Trueاستخدام bool("false")قارن النص بعد تحويله إلى حروف صغيرة
مفاتيح API ظاهرة في GitHubرفع ملف الأسرار أو تتبعه مسبقًاإلغاء المفتاح وتدويره ثم معالجة تاريخ المستودع

إذا لم تكن متأكدًا من البيئة التي تشغّل بها بايثون، ابدأ بشرح تثبيت بايثون ثم عد إلى هذا المثال.

الأسئلة الشائعة عن ملف .env في بايثون

هل ملف .env يخفي مفاتيح API عن الآخرين؟

لا. هو يفصل القيم عن الكود فقط، وليس ملفًا مشفرًا. من يستطيع قراءة الملف يستطيع قراءة المفاتيح. استخدم صلاحيات الملفات ووسائل إدارة الأسرار المناسبة، ولا تنشر القيم الحساسة.

هل يلزم إنشاء .venv لاستخدام python-dotenv؟

لا، لكن البيئة الافتراضية .venv مفيدة لفصل مكتبات المشروع. يمكن استخدام .env مع البيئة الافتراضية أو بدونها.

لماذا تعيد os.getenv قيمة None رغم وجود API_KEY في الملف؟

غالبًا لم يُحمّل الملف قبل القراءة، أو أن المسار مختلف، أو اسم المتغير غير مطابق. ابدأ بكود فحص المسار في هذا المقال ولا تطبع المفتاح نفسه.

هل يجب استخدام override=True دائمًا؟

لا. الإعداد الافتراضي override=False مناسب عادة لأنه يُبقي أولوية إعدادات النظام أو الاستضافة. استخدم True فقط إذا أردت عمدًا استبدالها بقيم الملف.

هل يمكن تشغيل المشروع دون وجود ملف .env؟

نعم إذا كانت متغيرات البيئة المطلوبة مثل API_KEY مضبوطة من نظام التشغيل أو منصة الاستضافة. المثال التطبيقي صُمم ليدعم الحالتين.

هل رفع .env.example إلى GitHub آمن؟

نعم إذا احتوى على أسماء المتغيرات وقيم وهمية فقط. لا تنسخ إليه قيمة مفتاح حقيقي أو كلمة مرور مستخدمة فعليًا.

إرسال تعليق

أحدث أقدم