تخطَّ إلى المحتوى

٩ يوليو ٢٠٢٦ · آخر تحديث: ١٩ أكتوبر ٢٠٢٦

حل خطأ IndexError و KeyError في بايثون بالعربي

شارك المقال:

خطآن يتصدّران شاشة كل متعلّم بايثون في أسابيعه الأولى:

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 listpop() من قائمة فارغة

list assignment تفاجئ القادمين من جافاسكريبت: بايثون لا يمدّد القائمة تلقائيًا حين تكتب بعد نهايتها. لإضافة عنصر استخدم append.

المواضع الشائعة

الموضعالمثال الخاطئالصحيح
آخر عنصرitems[len(items)]items[-1]
حلقة يدويةrange(len(items) + 1)range(len(items))
قائمة فارغةfirst = items[0]افحص if items: أولًا
نتيجة splitparts[1] بلا تحققافحص len(parts)
نتيجة بحث أو APIresults[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.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ Python

الأسئلة الشائعة

لماذا آخر عنصر هو len(list) - 1 وليس len(list)؟

لأن الفهرسة تبدأ من الصفر: قائمة من 3 عناصر فهارسها 0 و1 و2. طلب الفهرس 3 (الذي يساويه len) يقع خارج الحدود ويرمي IndexError فورًا. هذه أشهر زلة off-by-one في البرمجة كلها، والحل الأنظف لآخر عنصر هو items[-1].

ما الفرق بين d[key] و d.get(key)؟

d[key] يرمي KeyError إذا كان المفتاح غائبًا، بينما d.get(key) يرجع None بهدوء، وd.get(key, default) يرجع قيمتك الافتراضية. استخدم الأقواس عندما يكون الغياب خطأً فعلًا تريد اكتشافه، وget عندما يكون الغياب حالة طبيعية.

لماذا KeyError على عمود موجود في pandas؟

غالبًا اسم العمود فيه مسافة أو حالة أحرف مختلفة. جرّبنا ملف CSV عنوانه 'Name , Age'، فصار اسم العمود 'Name ' بمسافة، وdf['Name'] رمى KeyError: 'Name'. اطبع list(df.columns) لترى الأسماء الحقيقية، ونظّفها بـ df.columns = df.columns.str.strip().

ما معنى KeyError: 0؟

طلبت المفتاح 0 من قاموس أو من Series في pandas كأنه قائمة. القاموس لا يعرف المواضع، بل المفاتيح فقط. وفي pandas، s[0] يبحث عن تسمية الصف 0 لا عن أول صف؛ إن كان الفهرس يبدأ برقم آخر يرمي KeyError: 0. استخدم s.iloc[0] للموضع.

اقرأ أيضًا

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