خطآن يتصدّران شاشة كل متعلّم بايثون في أسابيعه الأولى:
IndexError: list index out of range
KeyError: 'email'
كلاهما الفكرة نفسها بوجهين: طلبت شيئًا من مكان غير موجود. فهرسًا خارج القائمة، أو مفتاحًا خارج القاموس. كل رسالة في هذا المقال نسخناها من تشغيل فعلي على Python 3.13 وpandas 3.
اقرأ الـ traceback من الأسفل
Traceback (most recent call last):
File "app.py", line 9, in <module>
print(report(users))
~~~~~~^^^^^^^
File "app.py", line 5, in report
return users[0]["email"]
~~~~~~~~^^^^^^^^^
KeyError: 'email'
- السطر الأخير نوع الخطأ والقيمة المطلوبة: المفتاح
'email'غير موجود. - السطر الذي فوقه مكان الخطأ بالضبط (السطر 5)، والعلامات
^^^تشير للعملية التي فشلت: الوصول لـ["email"]، لا لـusers[0]. - الأسطر الأعلى هي الطريق الذي أوصلك إلى هناك.
ثم اطبع ما تظنه موجودًا قبل السطر مباشرة: print(len(items)) للقوائم، و
print(d.keys()) للقواميس. الخطأ يصبح واضحًا في ثوانٍ.
أولًا: IndexError، حدود التسلسل
القاعدة الذهبية: العدّ من صفر
tracks = ["HTML", "CSS", "JavaScript"]
# 0 1 2
print(tracks[2]) # JavaScript: آخر عنصر
print(tracks[3]) # IndexError: list index out of range
القائمة طولها 3، لكن آخر فهرس صالح هو 2 (len - 1).
أخوات الرسالة
| الرسالة | ماذا فعلت |
|---|---|
list index out of range | قرأت من قائمة بفهرس غير موجود |
tuple index out of range | الشيء نفسه مع tuple (غالبًا نتيجة دالة أو صف من قاعدة بيانات) |
string index out of range | فهرس خارج طول نص: "abc"[10] |
list assignment index out of range | كتبت على فهرس غير موجود: items[5] = x في قائمة أقصر |
pop from empty list | pop() من قائمة فارغة |
list assignment تفاجئ القادمين من جافاسكريبت: بايثون لا يمدّد القائمة تلقائيًا
حين تكتب بعد نهايتها. لإضافة عنصر استخدم append.
المواضع الشائعة
| الموضع | المثال الخاطئ | الصحيح |
|---|---|---|
| آخر عنصر | items[len(items)] | items[-1] |
| حلقة يدوية | range(len(items) + 1) | range(len(items)) |
| قائمة فارغة | first = items[0] | افحص if items: أولًا |
| نتيجة split | parts[1] بلا تحقق | افحص len(parts) |
| نتيجة بحث أو API | results[0] | قد تكون القائمة فارغة: افحصها |
الفهارس السالبة والشرائح
tracks[-1] # آخر عنصر، بلا حساب طول
tracks[-2] # قبل الأخير
tracks[5:10] # [] بهدوء: الشرائح لا ترمي IndexError أبدًا
الحذف داخل الحلقة: الفخ الكلاسيكي
nums = [1, 2, 3, 4]
for i in range(len(nums)):
if nums[i] % 2 == 0:
del nums[i] # IndexError: list index out of range
range(len(nums)) حُسب مرة واحدة على 4 عناصر، لكن القائمة تقصر مع كل حذف، فتصل
الحلقة لفهرس لم يعد موجودًا. ابنِ قائمة جديدة بدل الحذف أثناء المرور:
nums = [n for n in nums if n % 2 != 0]
الحلقة الصحيحة أصلًا لا تحتاج فهارس
for track in tracks: # بايثوني وآمن
print(track)
for i, track in enumerate(tracks): # إن احتجت الفهرس فعلًا
print(i, track)
(enumerate وzip بالتفصيل في درسهما.)
ثانيًا: KeyError، مفاتيح القاموس
user = {"name": "أحمد", "age": 17}
print(user["email"]) # KeyError: 'email'
الرسالة تطبع المفتاح الغائب نفسه. اقرأه: كثيرًا ما يكون السبب خطأً إملائيًا
("Email" مقابل "email")، أو مسافة، أو رقمًا مخزّنًا كنص ("1" مقابل 1).
طرق الوصول الآمن
# 1) get مع قيمة افتراضية: الأنسب حين يكون الغياب طبيعيًا
email = user.get("email", "لا يوجد بريد")
# 2) فحص العضوية حين يتفرّع المنطق
if "email" in user:
send_mail(user["email"])
# 3) try/except حين يكون الغياب استثناءً فعلًا
try:
email = user["email"]
except KeyError:
email = ask_user_for_email()
# 4) setdefault: اقرأ، وأنشئ إن لم يوجد
tags = post.setdefault("tags", [])
tags.append("python")
# 5) الحذف الآمن: del يرمي KeyError، وpop مع افتراضي لا يرمي
user.pop("token", None)
بيانات JSON متداخلة من API
data = response.json()
data["data"]["user"]["email"] # KeyError: 'user' إن تغيّر شكل الرد
لا تخمّن شكل البيانات: اطبعها مرة (print(data.keys()) أو pprint(data))، ثم
اقرأ بأمان في كل مستوى:
email = data.get("data", {}).get("user", {}).get("email")
وإن كان الحقل يجب أن يوجد، فدع الخطأ يظهر، لكن برسالة تشرح المتوقع بدل
KeyError عارٍ.
os.environ: متغير بيئة ناقص
import os
db_url = os.environ["DATABASE_URL"] # KeyError: 'DATABASE_URL'
يعمل على جهازك وينهار في الخادم أو داخل Docker؟ المتغير غير معرّف هناك. استخدم
os.environ.get("DATABASE_URL") لمتغير اختياري، وللمتغير الإلزامي أوقف البرنامج
برسالة واضحة عند الإقلاع:
db_url = os.environ.get("DATABASE_URL")
if not db_url:
raise SystemExit("DATABASE_URL غير معرّف: أضفه لملف .env أو لإعدادات الخادم")
defaultdict لقواميس التجميع
عند بناء «قاموس قوائم» يظهر KeyError مع أول إضافة لكل مفتاح جديد. defaultdict
يحلها من الجذر:
from collections import defaultdict
lessons_by_track = defaultdict(list)
for lesson in lessons:
lessons_by_track[lesson.track].append(lesson) # القائمة تُنشأ تلقائيًا
وللعدّ خصيصًا، Counter أنظف:
from collections import Counter
level_counts = Counter(l.level for l in lessons)
(القواميس وأدواتها بالتفصيل في درس القواميس.)
ثالثًا: KeyError في pandas
نصف من يبحث عن KeyError يعمل على جدول بيانات، لا على قاموس.
عمود «موجود» يرمي KeyError
import pandas as pd
df = pd.read_csv("students.csv") # العنوان في الملف: "Name , Age"
df["Name"] # KeyError: 'Name'
print(list(df.columns)) # ['Name ', ' Age']
العمود اسمه 'Name ' بمسافة في آخره، لأن ملف CSV فيه مسافة بعد الاسم. نظّف
الأسماء مرة واحدة بعد القراءة:
df.columns = df.columns.str.strip()
KeyError: 0: موضع أم تسمية؟
s = pd.Series([10, 20], index=[5, 6])
s[0] # KeyError: 0 (لا يوجد صف اسمه 0)
s.iloc[0] # 10 (أول صف حسب الموضع)
في pandas، الأقواس العادية تبحث عن تسمية الصف. بعد filter أو sort_values
لا تبقى التسميات 0 و1 و2، فيفشل s[0]. استخدم .iloc للموضع و.loc للتسمية.
(تجهيز البيانات بـ pandas في درس pandas.)
KeyError: 0 يظهر بالمعنى نفسه مع القاموس العادي: {"a": 1}[0]. القاموس لا يعرف
المواضع، بل المفاتيح فقط.
قائمة تحقّق سريعة
- اقرأ آخر سطرين في الـ traceback: نوع الخطأ والسطر، و
^^^تشير للعملية. - آخر فهرس صالح =
len - 1، والأفضل[-1]. - قبل
items[0]: هل يمكن أن تكون القائمة فارغة؟ - لا تحذف من قائمة أثناء المرور عليها بالفهارس: ابنِ قائمة جديدة.
- KeyError؟ اقرأ المفتاح في الرسالة، واطبع
.keys(). - الغياب طبيعي؟
getأوin. تجميع؟defaultdict. حذف؟pop(key, None). os.environ[...]ينهار في الخادم؟ المتغير غير معرّف هناك.- pandas؟
list(df.columns)و.str.strip()، و.ilocللمواضع.
الخلاصة
IndexError وKeyError ليسا عدوّين بل حارسا حدود: يوقفانك لحظة طلب ما ليس
موجودًا، بدل إعطائك قيمة خاطئة بصمت. اقرأ الرسالة، فهي تطبع الفهرس أو المفتاح
نفسه، ثم اطبع ما هو موجود فعلًا، وستجد الفرق في ثوانٍ.
الأساس في درس القوائم ودرس الاستثناءات، وأكمل سلسلة الأخطاء مع أخطاء بايثون الشائعة وحل SyntaxError.