> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أخطاء شائعة وأسلوب Python الصحيح Pythonic
- URL: https://arabroot.io/articles/اخطاء-شائعة-واسلوب-python-الصحيح-pythonic/
- Published: 2026-10-31T09:46:10.000Z
- Updated: 2026-10-31T10:45:36.000Z
- Description: خمسة عشر خطأً يقع فيها من يأتي إلى Python من Java وJavaScript وC#، كل واحد بتشغيل خاطئ حقيقي وإصلاحه وما يمسكه ruff منه، ثم خطة قراءة لأربعة أسابيع من التوثيق الرسمي.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

بعد شهر من العمل على خدمة الطلبات في المتجر الصغير الذي يبيع القهوة والتمور، صار في الفريق ثلاثة مطورين جاؤوا إلى Python من Java وJavaScript وC#، وكلهم يكتبون كوداً يعمل ويمر من الاختبارات. ولكن مراجعات الكود Code Review بدأت تتكرر فيها نفس الملاحظات: سلة عميل ظهرت فيها منتجات عميل آخر في بيئة التجربة، وخصم صفر في المئة تحول إلى خمسة، ومنتج اختفى من الطلب دون أي خطأ لأن قائمة الكميات كانت أقصر بعنصر، وطلب لم يحفظ لأن أحدهم نسي `await`. والمشترك بين هذه الأخطاء أنها ليست أخطاء برمجة عامة، فالمطور نفسه لا يقع فيها في لغته الأصلية، وإنما أخطاء من يكتب Python بعقلية لغة أخرى، فيفترض أن القيمة الافتراضية تنشأ مع كل استدعاء كما في JavaScript، أو أن `zip` يعترض إذا اختلف الطول كما يفعل الكثير من المكتبات في Java.

وهذا المقال الأخير في مرحلة [لغة Python للمطورين](https://arabroot.io/start/#stage-lang-python)، وفيه خمسة عشر خطأً من هذا النوع، لكل واحد ملف صغير شغلناه مرتين على Python 3.15.2: مرة بالطريقة الخاطئة ومرة بالإصلاح، ومخرج المرتين كما جاء، ثم ما يقوله ruff عن كل واحد، ثم خطة قراءة من المصادر الرسمية لمن يريد أن يكمل بعد هذه المرحلة. وإذا كنت تبحث عن أساسيات البرمجة نفسها فمكانها مقال [المتغيرات والشروط والحلقات والدوال](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D9%85%D8%AA%D8%BA%D9%8A%D8%B1%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B4%D8%B1%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AD%D9%84%D9%82%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AF%D9%88%D8%A7%D9%84/)، أما هنا فالقارئ يعرف البرمجة ويريد أن يكتب Python كما يكتبها أهلها. وسوف نناقش في هذا المقال ما يلي:

- ما معنى الكود «الصحيح بأسلوب Python» Pythonic، ولماذا ليس مسألة ذوق.
- أخطاء التقييم Evaluation: القيمة الافتراضية القابلة للتغيير، والـ Closures داخل الحلقات.
- أخطاء القوائم والمراجع: الحذف أثناء المرور، والصفوف المشتركة، والـ Generator الذي يستهلك مرة واحدة.
- أخطاء تخفي الخطأ: `except` بدون نوع، و`or` مع الصفر، و`zip` بأطوال مختلفة، ونسيان `await`.
- أخطاء الأسماء والاستيراد: المتغير العام، والاسم الذي يغطي دالة مدمجة، والاستيراد الدائري، وتشغيل وحدة من حزمة كسكربت.
- خطأ في الأداء بدمج النصوص، وخطأ في الوقت بتاريخ بلا منطقة زمنية.
- أي هذه الأخطاء يمسكها ruff وأيها لا يمسكه إلا الاختبار، وخطة قراءة لأربعة أسابيع من التوثيق الرسمي.

## ما معنى Pythonic؟ الأسلوب Style الذي تتوقعه اللغة

الكلمة Pythonic تعني كوداً يستعمل اللغة كما صممت أن تستعمل، وليس فقط كوداً يعمل. وأقرب ما يكون إلى تعريف رسمي لها هو [PEP 20](https://peps.python.org/pep-0020/?ref=arabroot.io) المعروف باسم The Zen of Python، وهو تسعة عشر سطراً كتبها Tim Peters وتظهر إذا كتبت `import this`، ومنها «الصريح أفضل من الضمني» Explicit is better than implicit، و«يجب ألا تمر الأخطاء بصمت» Errors should never pass silently، و«يجب أن تكون هناك طريقة واحدة واضحة لعمل الشيء، ويفضل أن تكون واحدة فقط». ومعه [PEP 8](https://peps.python.org/pep-0008/?ref=arabroot.io) الذي يحدد أسلوب الكتابة من أسماء ومسافات وطول أسطر، وهو ما يطبقه ruff تلقائياً.

وقد يتساءل البعض: إذا كان الكود يعمل، فلماذا أهتم بالأسلوب؟ والإجابة أن أغلب الأخطاء في هذا المقال هي بالضبط مخالفة لسطر من هذه الأسطر، فـ `except` بدون نوع يجعل الأخطاء تمر بصمت، و`or` للقيمة الافتراضية ضمني حيث يجب أن يكون صريحاً، و`zip` بدون `strict=True` يقبل ما لا يجب أن يقبل. أي أن الأسلوب في Python ليس مسألة جمال، وإنما مجموعة قواعد تعلمها المجتمع من أخطاء حقيقية، والطريقة «غير الـ Pythonic» غالباً هي الطريقة التي تحمل خطأً ينتظر أن يظهر. والشكل التالي يلخص الأخطاء الخمسة عشر وما ظهر في كل تشغيل:

![جدول من خمسة عشر صفاً بأعمدة الخطأ وما ظهر في التشغيل الخاطئ والإصلاح وقاعدة ruff: القيمة الافتراضية القابلة للتغيير B006، والـ Closures داخل حلقة B023، والحذف أثناء المرور بدون قاعدة، و[[0] * 3] * 3 بدون قاعدة، وexcept بدون نوع E722 وF821، وتعديل متغير عام F823، والقيمة الافتراضية بـ or بدون قاعدة، وzip بأطوال مختلفة B905، والـ Generator مرتين بدون قاعدة، والاسم الذي يغطي دالة مدمجة A001، ودمج النصوص 49.2 ms مقابل 18.3 ms، ونسيان await، والاستيراد الدائري، وتشغيل وحدة من حزمة كسكربت](https://arabroot.io/content/images/2026/10/python-idioms-01-mistakes.webp)

خمسة عشر خطأً، ومخرج كل واحد بالطريقة الخاطئة، وما أمسكه ruff منها

والملفات كلها في المجلد `lab/m` مع المقال، وكل ملف يأخذ معاملاً واحداً `wrong` أو `fixed`، والملف `run.sh` يشغل كل واحد بالطريقتين داخل Container من الـ Image الرسمية، ويطبع المخرج وآخر أسطر الخطأ ورمز الخروج:

```bash
docker run --rm --cpus 2 --memory 512m -v "$PWD/lab:/lab" python:3.15.2-slim-trixie sh /lab/run.sh
```

## لماذا تقع في هذه الأخطاء؟ ثلاث قواعد Rules في نموذج اللغة

قبل أن نمر على الأخطاء واحداً واحداً من المفيد أن ترى أنها لا تأتي من خمسة عشر مكاناً مختلفاً، وإنما من ثلاث قواعد في تصميم Python، إذا فهمتها توقعت أغلب هذه الأخطاء قبل أن تقع فيها. القاعدة الأولى أن الاسم يشير إلى كائن ولا يحتويه، وهذا ما رأيناه بالتفصيل في مقال الأنواع والذاكرة: الإسناد يعلق اسماً، والتمرير إلى دالة يمرر المرجع، والضرب `*` على قائمة يكرر المراجع، فكل خطأ فيه «تعديل ظهر في مكان لم أتوقعه» هو هذه القاعدة.

والقاعدة الثانية أن `def` و`class` و`import` أوامر تنفذ وقت التشغيل، وليست تعريفات يقرؤها المترجم قبل أن يبدأ البرنامج كما في Java أو C#. فالسطر `def` ينفذ مرة واحدة عندما يصل إليه المفسر، وينشئ كائن دالة، ويحسب القيم الافتراضية في تلك اللحظة، والسطر `import` ينفذ الملف المستورد من أوله إلى آخره عند أول استيراد، ولذلك يحدث الاستيراد الدائري: الوحدة الأولى لم تكمل تنفيذ أسطرها عندما طلبت الثانية اسماً منها. وأما نطاق الأسماء Scope فيتحدد عند ترجمة الدالة إلى Bytecode كما رأينا في المقال الأول، بقاعدة البحث المعروفة: المحلي ثم الدالة المحيطة ثم العام ثم المدمج، ولذلك يصبح الاسم الذي يسند إليه في أي سطر من الدالة محلياً في الدالة كلها.

والقاعدة الثالثة أن لكل قيمة في Python «صدقاً» Truthiness في الشروط: الصفر والنص الفارغ والقائمة الفارغة والقاموس الفارغ و`None` كلها كاذبة، وكل شيء آخر صادق تقريباً. وهذا يجعل `if items:` طريقة مختصرة للسؤال عن قائمة غير فارغة، ولكنه يجعل `or` وكل شرط مختصر يعامل الصفر الصحيح كأنه غياب للقيمة. وإذا وضعت هذه القواعد الثلاث أمامك وأنت تقرأ كوداً من زميل قادم من لغة أخرى، فسوف تجد أن أغلب ملاحظاتك تنتمي إلى واحدة منها.

## أخطاء وقت التقييم Evaluation Time

أول مجموعة من الأخطاء تأتي من سؤال واحد: متى تحسب Python القيمة التي كتبتها؟ في JavaScript تحسب القيمة الافتراضية لمعامل الدالة عند كل استدعاء، وفي Python تحسب مرة واحدة فقط، عندما ينفذ سطر `def` نفسه. وفي الـ Closures يحدث العكس تماماً: الدالة الداخلية لا تحفظ قيمة المتغير وقت إنشائها، وإنما تحفظ المتغير نفسه، وتقرأ قيمته وقت استدعائها. والشكل التالي يبين الحالتين:

![صندوقان: الأول الدالة add_item مع cart=[] حيث يشير استدعاء العميل الأول واستدعاء العميل الثاني إلى نفس القائمة add_item.__defaults__[0] فتصبح النتيجة قهوة ثم قهوة وتمر، والإصلاح cart=None، والثاني ثلاث دوال lambda في حلقة تعطي cardamom=1500 ثلاث مرات، ومع lambda name=name تعطي coffee=4500 وdates=990 وcardamom=1500، وتحتهما قاعدة أن ما يكتب في سطر def يقيم مرة واحدة وما في جسم الدالة يقيم عند كل استدعاء](https://arabroot.io/content/images/2026/10/python-idioms-02-binding.webp)

القيمة الافتراضية تقيم مرة واحدة، والـ Closure يقرأ المتغير عند الاستدعاء

### 1\. القيمة الافتراضية القابلة للتغيير Mutable Default Argument

الدالة التالية تضيف منتجاً إلى سلة، وإذا لم تمرر سلة تنشئ سلة جديدة، أو هكذا يظن من كتبها:

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print("customer A:", add_item("قهوة"))
print("customer B:", add_item("تمر"))
```

والمخرج بالطريقة الخاطئة ثم بالإصلاح:

```text
--- wrong
customer A: ['قهوة']
customer B: ['قهوة', 'تمر']
(exit 0)
--- fixed
customer A: ['قهوة']
customer B: ['تمر']
(exit 0)
```

العميل الثاني وجد قهوة العميل الأول في سلته. والسبب أن `[]` في سطر `def` تنشئ قائمة واحدة عند تعريف الدالة، وتحفظ في الخاصية `add_item.__defaults__`، وكل استدعاء بدون سلة يستعمل نفس القائمة ويضيف إليها. وفي خدمة ويب تعمل أياماً دون إعادة تشغيل تكبر هذه القائمة مع كل طلب، وتنتقل بيانات عميل إلى آخر، وهذا خطأ أمني وليس خطأً في العرض فقط. والإصلاح الذي تجده في كل كود Python ناضج:

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
```

والقيمة `None` لا تتغير، فلا مشكلة في مشاركتها، والقائمة الجديدة تنشأ داخل جسم الدالة عند كل استدعاء. وهذا السؤال موجود في [الأسئلة الشائعة في التوثيق الرسمي](https://docs.python.org/3/faq/programming.html?ref=arabroot.io#why-are-default-values-shared-between-objects)، وruff يمسكه بالقاعدة `B006` كما سوف نرى.

### 2\. الربط المتأخر Late Binding في الـ Closures

نبني قائمة دوال، كل واحدة تنسق سعر منتج، ثم نستدعيها بعد الحلقة:

```python
prices = {"coffee": 4500, "dates": 990, "cardamom": 1500}
formatters = [lambda: f"{name}={prices[name]}" for name in prices]
print([f() for f in formatters])
```

```text
--- wrong
['cardamom=1500', 'cardamom=1500', 'cardamom=1500']
(exit 0)
--- fixed
['coffee=4500', 'dates=990', 'cardamom=1500']
(exit 0)
```

الدوال الثلاث أعطت الهيل، لأن كل `lambda` تحفظ مرجعاً إلى المتغير `name` وليس قيمته، وعندما استدعيناها كانت الحلقة قد انتهت والمتغير يحمل آخر قيمة. ويفاجئ هذا مطوري JavaScript بالذات، لأن `let` في حلقة `for` هناك تنشئ متغيراً جديداً في كل دورة، أما في Python فالمتغير واحد. والإصلاح الأقصر أن تجعل القيمة معاملاً افتراضياً `lambda name=name: ...`، فتستفيد من الخطأ الأول نفسه: القيمة الافتراضية تقيم مرة واحدة عند إنشاء كل دالة، أي في كل دورة. والإصلاح الأوضح للقارئ هو `functools.partial` أو دالة تنشئ الدالة، وهذا السؤال أيضاً في [الأسئلة الشائعة](https://docs.python.org/3/faq/programming.html?ref=arabroot.io#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result).

## أخطاء القوائم والمراجع References

رأينا في مقال الأنواع والذاكرة أن الاسم في Python يشير إلى كائن، وأن أكثر من اسم قد يشير إلى نفس الكائن، والأخطاء الثلاثة التالية كلها نتيجة مباشرة لذلك:

![صندوقان: الأول ثلاثة صفوف row 0 وrow 1 وrow 2 تشير كلها بأسهم إلى قائمة واحدة [5, 0, 0]، والمخرج [[5, 0, 0], [5, 0, 0], [5, 0, 0]]، والإصلاح بـ comprehension، والثاني جدول خطوات الحذف أثناء المرور: الفهرس 0 يحذف c1، والفهرس 1 يصل إلى h1 فلا يفحص d1 أبداً، والفهرس 2 يحذف x1، والنتيجة الخاطئة d1 وh1 والصحيحة h1](https://arabroot.io/content/images/2026/10/python-idioms-03-lists.webp)

الصفوف الثلاثة قائمة واحدة، والحذف يزيح العناصر فتقفز الحلقة فوق واحد منها

### 3\. الحذف من قائمة أثناء المرور عليها Mutating While Iterating

نريد أن نحذف من السلة كل سطر كميته صفر قبل حساب الطلب:

```python
cart = [{"sku": "c1", "qty": 0}, {"sku": "d1", "qty": 0}, {"sku": "h1", "qty": 2}, {"sku": "x1", "qty": 0}]
for line in cart:
    if line["qty"] == 0:
        cart.remove(line)
print([l["sku"] for l in cart])
```

```text
--- wrong
['d1', 'h1']
(exit 0)
--- fixed
['h1']
(exit 0)
```

بقي السطر `d1` وكميته صفر. والسبب أن حلقة `for` على القائمة تتقدم بفهرس داخلي: في الدورة الأولى عند الفهرس 0 حذفت `c1`، فانزاحت كل العناصر خطوة إلى اليسار، وصار `d1` في الفهرس 0، ولكن الحلقة انتقلت إلى الفهرس 1 فوجدت `h1`، ولم تر `d1` أبداً. ولا يوجد أي خطأ أو تحذير، والنتيجة خاطئة فقط عندما يوجد سطران متتاليان بكمية صفر، أي أن الاختبار البسيط بسطر واحد سوف ينجح. والإصلاح أن تبني قائمة جديدة بالشرط بدل أن تعدل القائمة التي تمر عليها:

```python
cart = [line for line in cart if line["qty"] != 0]
```

وإذا كان لا بد أن تعدل نفس الكائن لأن أسماء أخرى تشير إليه فاكتب `cart[:] = [...]`، أو مر على نسخة منها `for line in cart[:]`. ولاحظ أن القاموس أكثر صرامة من القائمة هنا، فتعديل حجم قاموس أثناء المرور عليه يرمي `RuntimeError` مباشرة. وفي كود قائم تبحث عن هذا الخطأ بنمط واضح: حلقة `for x in items` وفي جسمها `items.remove` أو `items.pop` أو `del items[...]`، وأي واحدة منها تستحق أن تتوقف عندها في المراجعة.

### 4\. الصفوف المشتركة Shared References في \[\[0\] \* 3\] \* 3

جدول بسيط لكميات ثلاثة رفوف في ثلاثة فروع، ثم نضع 5 في الخانة الأولى:

```python
shelves = [[0] * 3] * 3
shelves[0][0] = 5
print(shelves)
```

```text
--- wrong
[[5, 0, 0], [5, 0, 0], [5, 0, 0]]
(exit 0)
--- fixed
[[5, 0, 0], [0, 0, 0], [0, 0, 0]]
(exit 0)
```

العامل `*` على قائمة يكرر المراجع وليس الكائنات، فالقائمة الخارجية فيها ثلاثة مراجع لنفس القائمة الداخلية، وأي تعديل في صف يظهر في الثلاثة. ومع الأعداد في `[0] * 3` لا توجد مشكلة لأن الأعداد لا تتغير، فالإسناد يضع عدداً جديداً في الخانة، أما مع قائمة داخل قائمة فالمشكلة مضمونة. والإصلاح `[[0] * 3 for _ in range(3)]`، لأن الـ Comprehension ينفذ `[0] * 3` في كل دورة فينشئ قائمة جديدة لكل صف، وهذا السؤال في [الأسئلة الشائعة](https://docs.python.org/3/faq/programming.html?ref=arabroot.io#how-do-i-create-a-multidimensional-list) أيضاً. ونفس الخطأ يأتي بأشكال أخرى أقل وضوحاً، مثل `dict.fromkeys(skus, [])` الذي يعطي كل المفاتيح نفس القائمة، والحل هناك `{sku: [] for sku in skus}` أو `collections.defaultdict(list)`.

### 5\. الـ Generator يستهلك Exhausted مرة واحدة

نحسب مجموع أسطر الطلب، ثم أكبر سطر، من نفس التعبير:

```python
lines = [(990, 3), (4500, 1)]
totals = (p * q for p, q in lines)
print("subtotal:", sum(totals))
print("largest line:", max(totals, default="(nothing)"))
```

```text
--- wrong
subtotal: 7470
largest line: (nothing)
(exit 0)
--- fixed
subtotal: 7470
largest line: 4500
(exit 0)
```

الأقواس العادية حول التعبير تنشئ Generator وليس قائمة، والمولد ينتج قيمه مرة واحدة فقط، فاستهلكها `sum` كلها، ووجده `max` فارغاً فأرجع القيمة الافتراضية. ولو لم نكتب `default` لرمى `max` خطأ `ValueError` وكان الخطأ أوضح. والمطور القادم من Java يتوقع هذا مع Streams لأن الـ Stream في Java يرمي `IllegalStateException` إذا استعملته مرتين، أما في Python فالمولد الفارغ لا يعترض، وإنما يتصرف كأنه لا يوجد فيه شيء. والإصلاح أن تستعمل قائمة بالأقواس المربعة إذا كنت تحتاج القيم أكثر من مرة، وأن تحتفظ بالمولد للحالات التي تمر فيها على البيانات مرة واحدة، كقراءة ملف كبير كما رأينا في مقال الذاكرة.

## أخطاء تخفي الخطأ Silent Failures

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

![ثلاثة صناديق: قيمة خاطئة بصمت في ثمانية أخطاء ينتهي فيها البرنامج بـ exit 0، وتحذير فقط في خطأ واحد هو نسيان await، واستثناء واضح في خمسة أخطاء ينتهي فيها البرنامج بـ exit 1، وتحتها أن ruff بقواعد E وF وB وA وRUF وPLW أمسك 6 أخطاء من 14 بقواعده هذه هي B006 وB023 وE722 وF823 وB905 وA001، والثمانية الباقية لا يمسكها إلا اختبار أو مراجعة](https://arabroot.io/content/images/2026/10/python-idioms-04-silent.webp)

ثمانية من الخمسة عشر أعطت قيمة خاطئة بدون أي رسالة

### 6\. except بدون نوع Bare Except

```python
def price_of(sku, catalog):
    try:
        return catalgo[sku]          # typo: NameError
    except:
        return 0
```

```text
--- wrong
price: 0
(exit 0)
--- fixed
  File "/tmp/m/m05_bare_except.py", line 11, in price_of_fixed
    return catalgo[sku]
           ^^^^^^^
NameError: name 'catalgo' is not defined. Did you mean: 'catalog'?
(exit 1)
```

كتب المطور `except:` لكي يرجع صفراً إذا لم يكن المنتج موجوداً، ولكن الكلمة `except` وحدها تلتقط كل شيء، بما فيه `NameError` الناتج عن خطأ إملائي في اسم المتغير، فصار سعر كل منتج صفراً ولا يوجد أي أثر في السجل. والإصلاح أن تلتقط النوع الذي تتوقعه فقط، أي `except KeyError`، فظهر الخطأ الإملائي فوراً، ولاحظ أن Python اقترحت الاسم الصحيح بنفسها `Did you mean: 'catalog'?`. والـ `except:` بدون نوع تلتقط حتى `KeyboardInterrupt` و`SystemExit`، فقد تمنعك من إيقاف البرنامج بـ Ctrl+C، وحتى `except Exception` واسعة جداً إلا في الطبقة العليا من الخدمة التي تسجل الخطأ وترد بـ 500\. وهذا تطبيق مباشر لسطر «يجب ألا تمر الأخطاء بصمت» في PEP 20.

### 7\. القيمة الافتراضية بـ or تأكل الصفر

```python
requests = [{"discount": 10}, {"discount": 0}, {}]
for body in requests:
    discount = body.get("discount") or 5      # default 5% when missing
    print(body, "->", discount)
```

```text
--- wrong
{'discount': 10} -> 10
{'discount': 0} -> 5
{} -> 5
(exit 0)
--- fixed
{'discount': 10} -> 10
{'discount': 0} -> 0
{} -> 5
(exit 0)
```

الطلب الذي أرسل خصماً صفراً بوضوح حصل على خصم 5%، لأن `or` في Python ترجع القيمة الثانية إذا كانت الأولى «كاذبة» Falsy، والصفر والنص الفارغ والقائمة الفارغة و`None` كلها كاذبة. وهذا النمط منتشر جداً لأنه قصير ويعمل في أغلب الحالات، ويشبه `||` في JavaScript التي فيها نفس المشكلة، ولذلك أضافت JavaScript العامل `??`. والإصلاح في Python أن تقول ما تقصده بالضبط: إذا كان المفتاح غائباً فاستعمل `body.get("discount", 5)`، وإذا كان المقصود أن `None` فقط تعني «غير موجود» فاكتب `5 if x is None else x`. وفي الخدمة الحقيقية لا تقرأ القاموس يدوياً أصلاً، وإنما تعرف الحقل في Pydantic بقيمة افتراضية كما فعلنا في مقال الخدمة. ونفس الفخ يظهر في الشروط المختصرة مثل `if not qty:` للتحقق من أن الكمية موجودة، فيرفض الكود الكمية صفراً بنفس رسالة الكمية الغائبة، والصيغة الصريحة `if qty is None:` تفرق بين الحالتين.

### 8\. zip بأطوال مختلفة يحذف بصمت

```python
skus = ["c1", "d1", "h1"]
qtys = [2, 1]                # one quantity is missing from the form
print(dict(zip(skus, qtys)))
```

```text
--- wrong
{'c1': 2, 'd1': 1}
(exit 0)
--- fixed
  File "/tmp/m/m08_zip_truncates.py", line 8, in <module>
    print(dict(zip(skus, qtys, strict=True)))
          ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zip() argument 2 is shorter than argument 1
(exit 1)
```

الدالة `zip` تتوقف عند أقصر قائمة، فاختفى المنتج `h1` من الطلب لأن النموذج أرسل كميتين فقط، ولم يعترض أحد. ومنذ Python 3.10 يوجد المعامل `strict=True` كما في [توثيق الدالة zip](https://docs.python.org/3/builtins/functions.html?ref=arabroot.io#zip)، فيرمي `ValueError` إذا اختلف الطول، وهذا ما تريده في كل مكان تقترن فيه قائمتان يجب أن تكونا بنفس الطول. وإذا كان الاختلاف مقصوداً وتريد ملء الناقص بقيمة فاستعمل `itertools.zip_longest`.

### 9\. نسيان await

```python
saved = []

async def save_order(order_id):
    await asyncio.sleep(0.01)
    saved.append(order_id)

async def main():
    save_order(1)
    print("saved:", saved)
```

```text
--- wrong
saved: []
/tmp/m/m12_forgot_await.py:11: RuntimeWarning: coroutine 'save_order' was never awaited
  save_order(1)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
(exit 0)
--- fixed
saved: [1]
(exit 0)
```

رأينا في مقال التزامن أن استدعاء دالة `async` لا ينفذها، وإنما ينشئ Coroutine ينتظر أن ينفذه أحد بـ `await`، فبدون `await` لم يحفظ الطلب، والقائمة فارغة. والفرق عن JavaScript أن الدالة `async` هناك تبدأ التنفيذ فوراً عند الاستدعاء حتى بدون `await`، أما في Python فلا تبدأ أبداً. والشيء الوحيد الذي ظهر هو `RuntimeWarning` في stderr مع رمز خروج 0، فإذا كانت خدمتك لا ترسل التحذيرات إلى السجل، أو كان السجل مليئاً بغيرها، فلن يراه أحد. وmypy يمسك هذا الخطأ في أغلب الحالات إذا كانت الدوال مكتوبة بتلميحات، لأن القيمة غير المستعملة من نوع `Coroutine`، وفي الاختبارات شغل pytest مع `-W error::RuntimeWarning` لكي يتحول التحذير إلى فشل.

## أخطاء الأسماء Names والاستيراد Imports

### 10\. تعديل متغير عام داخل دالة UnboundLocalError

```python
orders_today = 0

def record_order():
    orders_today += 1
    return orders_today
```

```text
--- wrong
  File "/tmp/m/m06_unbound_local.py", line 7, in record_order
    orders_today += 1
    ^^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'orders_today' where it is not associated with a value
(exit 1)
--- fixed
2
(exit 0)
```

هذا خطأ يحير القادمين من JavaScript وC#، لأن المتغير موجود بوضوح في أعلى الملف. والسبب أن Python تحدد نطاق كل اسم داخل الدالة وقت الترجمة إلى Bytecode وليس وقت التشغيل: أي اسم يسند إليه في أي مكان داخل الدالة يعتبر محلياً في الدالة كلها، و`+=` إسناد، فصار `orders_today` محلياً، وعندما حاولت الدالة قراءته قبل الإسناد لم تجد له قيمة. والكلمة `global` تحل الخطأ، ولكنها ليست الإصلاح الذي نقترحه، لأن متغيراً عاماً يعدل من الدوال هو حالة مشتركة مخفية، ورأينا في مقال التزامن ماذا يحدث لها مع الخيوط. والإصلاح الذي في المختبر أن تضع الحالة في كائن يملكها ويعدلها، فطبع `2` بعد استدعاءين. وتفاصيل القاعدة في [الأسئلة الشائعة](https://docs.python.org/3/faq/programming.html?ref=arabroot.io#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value).

### 11\. اسم يغطي دالة مدمجة Shadowing a Built-in

```python
list = ["قهوة", "تمر"]                 # the name of the built-in type
print(len(list))
items = list(range(3))
```

```text
--- wrong
2
Traceback (most recent call last):
  File "/tmp/m/m10_shadow_builtin.py", line 6, in <module>
    items = list(range(3))
TypeError: 'list' object is not callable
(exit 1)
--- fixed
2
[0, 1, 2]
(exit 0)
```

الأسماء `list` و`dict` و`id` و`type` و`sum` و`input` ليست كلمات محجوزة في Python، وإنما دوال وكلاسات في نطاق المدمجات Built-ins، فيمكنك أن تسند إليها بلا أي اعتراض، ومن تلك اللحظة يغطي اسمك الاسم المدمج في نفس النطاق. والسطر الأول عمل، والخطأ ظهر بعد سطرين في مكان لا علاقة له بالسبب، وفي ملف حقيقي قد يظهر بعد مئة سطر. والإصلاح اسم يقول ما هو الشيء مثل `products`، وruff يمسكه بالقاعدة `A001` من مجموعة flake8-builtins، وهي ليست في الإعداد الافتراضي، فأضفها إلى `select`. ولاحظ أن `id` بالذات اسم مغر جداً في كود قواعد البيانات، والاسم `order_id` أوضح على أي حال.

### 12\. الاستيراد الدائري Circular Import ورسالة مضللة

الوحدة `orders` تستورد دالة الضريبة من `pricing`، و`pricing` تستورد نسبة الضريبة الافتراضية من `orders`:

```python
# circ_wrong/orders.py
from pricing import vat_halalas

def DEFAULT_RATE():
    return 15

# circ_wrong/pricing.py
from orders import DEFAULT_RATE

def vat_halalas(subtotal):
    return (subtotal * DEFAULT_RATE() + 50) // 100
```

```text
--- wrong
    from pricing import vat_halalas
  File "/tmp/m/circ_wrong/pricing.py", line 1, in <module>
    from orders import DEFAULT_RATE
ImportError: cannot import name 'DEFAULT_RATE' from 'orders' (consider renaming '/tmp/m/circ_wrong/orders.py' if it has the same name as a library you intended to import)
(exit 1)
--- fixed
446
(exit 0)
```

عندما بدأ تحميل `orders` وصل في سطره الأول إلى استيراد `pricing`، فبدأ تحميلها، و`pricing` طلبت `DEFAULT_RATE` من `orders` التي لم يكمل تحميلها بعد، فلم يجد الاسم. ولاحظ أن الرسالة في هذا الإصدار تقترح أن تغير اسم الملف ظناً أنه يغطي مكتبة بنفس الاسم، وهذا اقتراح مضلل في حالتنا، فالسبب الحقيقي هو الدائرة. والإصلاح أن تقطع الدائرة بوحدة ثالثة لا تستورد شيئاً من الاثنتين، فوضعنا الثابت في `settings.py` تستورده `pricing`، فأعطى البرنامج 446\. والاستيراد داخل الدالة يعمل أيضاً، ولكنه يخفي المشكلة بدل أن يحل تصميمها، وتفاصيل كيف يحمل Python الوحدات في [مرجع نظام الاستيراد](https://docs.python.org/3/reference/import.html?ref=arabroot.io).

![صندوقان: الأول استيراد دائري بين orders.py وpricing.py بسهمين: from pricing import vat_halalas ثم from orders import DEFAULT_RATE، ورسالة ImportError التي تقترح تغيير الاسم، والإصلاح بوحدة ثالثة settings.py والنتيجة 446، والثاني حزمة shop فيها __init__.py وpricing.py وcli.py مع استيراد نسبي، وتشغيل python shop/cli.py يعطي attempted relative import with no known parent package، وتشغيل python -m shop.cli يعطي VAT on 2970: 446](https://arabroot.io/content/images/2026/10/python-idioms-06-imports.webp)

رسالة الاستيراد الدائري تقترح سبباً آخر، والاستيراد النسبي يحتاج python -m

### ماذا لو What If: استعملنا lazy import في الاستيراد الدائري؟

رأينا في المقال الأول أن Python 3.15 أضافت الاستيراد الكسول بالكلمة `lazy`، فماذا يحدث إذا غيرنا السطر الأول في `orders.py` وحده إلى `lazy from pricing import vat_halalas` وتركنا الدائرة كما هي؟

```text
### circular import with lazy (3.15)
446
(exit 0)
```

عمل البرنامج وأعطى 446، والسبب أن `orders` لم تحمل `pricing` عند سطر الاستيراد، فأكملت تعريف `DEFAULT_RATE`، وعندما استدعى `main.py` الدالة `vat_halalas` حملت `pricing` في تلك اللحظة، وطلبت من `orders` اسماً صار موجوداً. ولكن لا تقم باستعمال `lazy` كإصلاح للدائرة، والسبب أن الدائرة نفسها ما زالت موجودة في التصميم، وأي سطر في المستقبل يستعمل `vat_halalas` أثناء تحميل `orders` سوف يعيد الخطأ، ومعه صعوبة أكبر في فهمه لأن ترتيب التحميل صار يعتمد على أول استعمال وليس على ترتيب الأسطر. فالـ `lazy` أداة لتسريع البدء كما قسناها، والوحدة الثالثة `settings.py` هي إصلاح التصميم.

### 13\. تشغيل وحدة من حزمة كسكربت Relative Import

```python
# rel/shop/cli.py
from .pricing import vat_halalas

print("VAT on 2970:", vat_halalas(2970))
```

```text
--- wrong
Traceback (most recent call last):
  File "/tmp/m/rel/shop/cli.py", line 1, in <module>
    from .pricing import vat_halalas
ImportError: attempted relative import with no known parent package
(exit 1)
--- fixed
VAT on 2970: 446
(exit 0)
```

الأمر `python shop/cli.py` يشغل الملف كسكربت مستقل اسمه `__main__`، فلا تعرف Python أنه جزء من الحزمة `shop`، والنقطة في `from .pricing` تعني «من نفس الحزمة» التي لا توجد. والإصلاح `python -m shop.cli` من المجلد الذي فيه الحزمة، فتحمل Python الحزمة أولاً ثم الوحدة داخلها. والطريقة الأفضل في مشروع حقيقي هي ما رأيناه في المقال الأول: قسم `[project.scripts]` في `pyproject.toml` يعرف أمراً يستدعي دالة في الحزمة، فلا يحتاج أحد أن يعرف مسار الملف أصلاً. والمطور القادم من Node.js يقع في هذا الخطأ كثيراً، لأن `node src/cli.js` هناك يعمل مع الاستيراد النسبي دائماً.

## خطأ في الأداء Performance: دمج النصوص

### 14\. بناء نص كبير String Building بـ += في حلقة

```python
rows = [f"{i},تمر سكري,990\n" for i in range(200_000)]

def concat():
    out = ""
    for r in rows:
        out += r
    return out

def join():
    return "".join(rows)
```

```text
--- wrong
concat: 3888890 chars in 49.2 ms
(exit 0)
--- fixed
join: 3888890 chars in 18.3 ms
(exit 0)
```

النتيجة نفسها في الطريقتين، ولكن `join` أسرع بحوالي 2.7 مرة على مئتي ألف سطر. والسبب أن النصوص في Python لا تتغير، فكل `+=` ينشئ نظرياً نصاً جديداً وينسخ كل ما قبله، وهذا يجعل الحلقة تربيعية في أسوأ الحالات. والفرق هنا أقل مما تتوقعه الشروحات القديمة لأن CPython يحسن `+=` على النص عندما يكون هناك مرجع واحد له فيكبره في مكانه، ولكن هذا تحسين في CPython وحده ولا تعد به اللغة، كما تذكر [الأسئلة الشائعة](https://docs.python.org/3/faq/programming.html?ref=arabroot.io#what-is-the-most-efficient-way-to-concatenate-many-strings-together)، و`"".join` هو الشكل المعروف الذي يحسب الطول الكلي مرة واحدة ويحجز مرة واحدة. ولملفات CSV الحقيقية لا تبن النص بنفسك أصلاً، وإنما استعمل الوحدة `csv` التي تعالج الفواصل وعلامات التنصيص.

## خطأ في الوقت Time Zones: تاريخ بلا منطقة زمنية

### 15\. المقارنة بين تاريخ Naive وتاريخ Aware

عرض التمر السكري ينتهي في آخر دقيقة من يوم 31 أكتوبر بتوقيت السعودية، والكود يسأل: هل العرض ما زال قائماً؟

```python
KSA = timezone(timedelta(hours=3))                    # Saudi time, no daylight saving
offer_ends = datetime(2026, 10, 31, 23, 59, tzinfo=KSA)

now = datetime.utcnow()                           # naive: no zone attached
print("now:", now.isoformat(), "tzinfo:", now.tzinfo)
print("offer still on:", now < offer_ends)
```

```text
--- wrong
/lab/m/m13_naive_datetime.py:10: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
  now = datetime.utcnow()                           # naive: no zone attached
now: 2026-10-31T10:09:13.957824 tzinfo: None
Traceback (most recent call last):
  File "/lab/m/m13_naive_datetime.py", line 12, in <module>
    print("offer still on:", now < offer_ends)
                             ^^^^^^^^^^^^^^^^
TypeError: can't compare offset-naive and offset-aware datetimes
(exit 1)
--- fixed
now: 2026-10-31T10:09:15.312017+00:00 | in KSA: 2026-10-31T13:09+03:00
offer still on: True
(exit 0)
```

في Python نوعان من التواريخ في نفس الكلاس: التاريخ الذي لا يعرف منطقته الزمنية ويسميه التوثيق Naive، و`tzinfo` فيه `None` كما طبع المخرج، والتاريخ «الواعي» Aware الذي يعرف فرقه عن UTC. والدالة `utcnow()` ترجع الوقت بتوقيت UTC ولكن في تاريخ بلا منطقة زمنية لا يقول إنه UTC، ولذلك صارت مهملة Deprecated كما قال التحذير في السطر الأول، وستحذف في إصدار قادم. والمقارنة بين النوعين ترمي `TypeError`، وهذا في الحقيقة أفضل ما يمكن أن يحدث، لأن الحالة الأخطر هي تاريخان بلا منطقة زمنية، أحدهما بتوقيت UTC من السيرفر والآخر بتوقيت السعودية من قاعدة البيانات، فتقارن Python بينهما بلا اعتراض، وينتهي العرض قبل موعده بثلاث ساعات أو بعده بثلاث. والإصلاح أن تكون كل التواريخ في الكود واعية: `datetime.now(UTC)` للوقت الحالي، والتحويل إلى توقيت السعودية عند العرض فقط بـ `astimezone`، كما في السطر الأخير الذي طبع الساعة `13:09+03:00`. والعمود `created_at` في خدمة الطلبات من نوع `timestamptz` لهذا السبب بالذات، فـ psycopg يرجعه تاريخاً واعياً. ولاحظ أن ruff لم يمسك هذا الخطأ بالقواعد التي استعملناها، ولكنه أمسكه عندما أضفنا مجموعة flake8-datetimez بالأمر `ruff check --select DTZ` فأعطى `` DTZ003 `datetime.datetime.utcnow()` used ``، وتفاصيل النوعين في [توثيق datetime](https://docs.python.org/3/library/datetime.html?ref=arabroot.io).

## اختبارات الحالات الحدية Edge Cases تمسك الأخطاء الصامتة

قلنا إن الأخطاء الصامتة لا يمسكها إلا اختبار يتحقق من القيمة، فلنكتب هذا الاختبار فعلاً. الملف `test_edges.py` في المختبر يحمل ثلاثة من الملفات الخاطئة ويسأل كل واحد سؤالاً واحداً من الحالات الحدية: هل يحصل كل عميل على سلة منفصلة؟ وهل يحذف سطران متتاليان بكمية صفر؟ وهل يبقى الخصم الصفري صفراً؟

```python
def test_two_customers_get_separate_carts(capsys):
    m = load("m01_mutable_default")
    assert m.add_item("x") == ["x"]

def test_zero_lines_in_a_row_are_removed(capsys):
    m = load("m03_modify_while_iterating")
    assert [l["sku"] for l in m.cart] == ["h1"]

def test_zero_discount_is_kept(capsys):
    load("m07_or_default")
    out = capsys.readouterr().out
    assert "{'discount': 0} -> 0" in out
```

شغلنا الاختبارات الثلاثة على النسخ الخاطئة ثم على النسخ المصلحة، باختيار النسخة من متغير البيئة `MODE`:

```text
### wrong
/tmp/test_edges.py:32: assert "{'discount': 0} -> 0" in "{'discount': 10} -> 10
{'discount': 0} -> 5
{} -> 5
"
FAILED test_edges.py::test_two_customers_get_separate_carts - AssertionError:...
FAILED test_edges.py::test_zero_lines_in_a_row_are_removed - AssertionError: ...
FAILED test_edges.py::test_zero_discount_is_kept - assert "{'discount': 0} ->...
3 failed in 0.04s
### fixed
...                                                                      [100%]
3 passed in 0.02s
```

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

## أيها يمسكه ruff؟ الفحص الآلي Linting مقابل الاختبار

شغلنا ruff على الملفات الخاطئة كلها بمجموعة قواعد أوسع من الإعداد الافتراضي:

```bash
ruff check --select E,F,B,A,RUF,PLW --output-format concise --no-cache m*.py circ_wrong rel | sort
```

```text
Found 14 errors.
[*] 3 fixable with `--fix` (2 unsafe fixes available with `--fix --unsafe-fixes`).
circ_wrong/orders.py:1:21: F401 [*] `pricing.vat_halalas` imported but unused
m01_mutable_default.py:4:29: B006 Do not use mutable data structures for argument defaults
m02_late_binding.py:5:30: B023 Function definition does not bind loop variable `name`
m02_late_binding.py:5:44: B023 Function definition does not bind loop variable `name`
m03_modify_while_iterating.py:10:21: E741 Ambiguous variable name: `l`
m03_modify_while_iterating.py:3:89: E501 Line too long (107 > 88)
m05_bare_except.py:11:16: F821 Undefined name `catalgo`
m05_bare_except.py:5:16: F821 Undefined name `catalgo`
m05_bare_except.py:6:5: E722 Do not use bare `except`
m06_unbound_local.py:7:9: F823 Local variable `orders_today` referenced before assignment
m08_zip_truncates.py:6:16: B905 `zip()` without an explicit `strict=` parameter
m10_shadow_builtin.py:4:5: A001 Variable `list` is shadowing a Python builtin
m11_string_concat.py:1:1: E401 [*] Multiple imports on one line
m12_forgot_await.py:1:1: E401 [*] Multiple imports on one line
```

وهذا التشغيل كان على الملفات الأربعة عشر الأولى، فأمسك ruff ستة منها: القيمة الافتراضية `B006`، والـ Closure `B023`، والـ `except` بدون نوع `E722` ومعه الخطأ الإملائي `F821` الذي أخفاه، والمتغير المحلي `F823`، و`zip` بدون `strict` `B905`، والاسم المدمج `A001`. والملاحظات الأخرى في المخرج حقيقية ولكنها في أسلوب الكتابة، مثل اسم المتغير `l` الذي يشبه الرقم 1\. وأما الثمانية الباقية من الأربعة عشر الأولى، أي الحذف أثناء المرور والصفوف المشتركة و`or` مع الصفر والمولد المستهلك ونسيان `await` والاستيراد الدائري والاستيراد النسبي ودمج النصوص، فلم يرها الفاحص في هذا الإعداد، والخطأ الخامس عشر لم تره هذه القواعد أيضاً وأمسكته مجموعة `DTZ` كما رأينا. والقواعد كلها موثقة في [قائمة قواعد ruff](https://docs.astral.sh/ruff/rules/?ref=arabroot.io)، وبعضها في وضع المعاينة Preview قد يمسك المزيد. والدرس العملي: ابدأ مشروعك بقواعد `B` و`A` إلى جانب `E` و`F`، فهي التي أمسكت أغلب أخطاء القادمين من لغات أخرى، ثم اعرف أن نصف الأخطاء تقريباً لا يمسكه إلا اختبار يتحقق من القيمة، مثل اختبار بسطرين متتاليين كميتهما صفر، أو خصم صفر صريح.

## من أي لغة يأتي كل خطأ؟ عادات Habits من لغتك الأولى

إذا كنت تراجع كود زميل، فمعرفة اللغة التي جاء منها تساعدك على توقع أخطائه قبل أن تقرأ الكود. فالمطور القادم من JavaScript يحمل معه ثلاث عادات تتحول إلى أخطاء في Python: الأولى أن القيمة الافتراضية للمعامل تحسب عند كل استدعاء، فيكتب `cart=[]` بثقة كاملة، والثانية أن `let` في الحلقة ينشئ متغيراً جديداً في كل دورة، فلا يتوقع مشكلة الـ Closures، والثالثة أن `||` للقيمة الافتراضية عادة يومية، فينقلها إلى `or` ومعها مشكلة الصفر. ويضاف إليها أن الدالة `async` في JavaScript تبدأ فور استدعائها، فنسيان `await` هناك يعني فقط أنك لا تنتظر النتيجة، أما في Python فيعني أن العمل لم يحدث أصلاً.

والمطور القادم من Java أو C# يأتي بعادات مختلفة: يكتب كلاساً لكل شيء مع دوال Getters وSetters بدل `dataclass`، ويستعمل `range(len(items))` لأنه تعود على حلقة الفهرس، ويلتقط `Exception` في كل مكان لأن المترجم في Java كان يجبره على معالجة الاستثناءات المفحوصة Checked Exceptions، فيبحث عن مكان يضع فيه `try`. ويتوقع أيضاً أن يعترض المترجم على متغير عام يعدل داخل دالة أو على تشغيل ملف خارج حزمته، ولذلك يفاجئه أن الخطأ يظهر وقت التشغيل فقط. وأما الاستيراد الدائري فيقع فيه القادمون من Java بالذات، لأن الكلاسات هناك يمكن أن يشير بعضها إلى بعض بحرية، والمترجم يحل كل الأسماء قبل التشغيل، بينما Python تنفذ الملفات بالترتيب.

وفي المقابل، هناك أخطاء لن يقع فيها هؤلاء لأن لغاتهم علمتهم الحذر منها: مطور Java يعرف أن تعديل قائمة أثناء المرور عليها يرمي `ConcurrentModificationException`، فإذا فعلها في Python فهو ينتظر خطأ لن يأتي، ومطور C# يعرف الفرق بين `DateTime` بنوعيه المحلي والعالمي، فيفهم مشكلة المنطقة الزمنية بسرعة. والخلاصة العملية لقائد الفريق: اجعل قائمة التحقق في المراجعة مختلفة قليلاً بحسب خلفية كاتب الكود في الأسابيع الأولى، وركز مع القادم من JavaScript على التقييم والقيم الكاذبة و`await`، ومع القادم من Java وC# على الاستثناءات الواسعة والاستيراد والأسلوب.

## كيف تدخل فريقاً من لغات أخرى إلى Python؟ Team Onboarding

معرفة الأخطاء وحدها لا تكفي فريقاً فيه ثلاثة مطورين جدد على اللغة، لأن الخطأ الذي تعرفه اليوم سوف ينساه أحدهم في طلب دمج مستعجل يوم الخميس. والطريقة التي تعمل في الواقع هي أن تحول كل ما يمكن تحويله إلى فحص آلي، وتترك للمراجعة البشرية ما لا يمكن فحصه آلياً. والخطوة الأولى إعداد ruff مكتوب في `pyproject.toml` بالقواعد التي أمسكت أخطاء هذا المقال، وmypy بالوضع الصارم للكود الجديد:

```toml
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "A", "DTZ", "RUF"]

[tool.mypy]
strict = true

[tool.pytest.ini_options]
filterwarnings = ["error::RuntimeWarning", "error::DeprecationWarning"]
```

والسطر الأخير يحول التحذيرات إلى أخطاء في الاختبارات، فنسيان `await` الذي لم يظهر إلا كتحذير في المختبر، واستعمال `utcnow()` المهملة، يصبحان فشلاً في CI بدل سطر في stderr لا يقرؤه أحد. ثم تشغل نفس الأدوات قبل كل Commit على جهاز المطور بأداة مثل pre-commit، لكي يرى الخطأ في ثوان وليس بعد عشر دقائق في CI، وفي CI مرة أخرى لكي لا يعتمد الفريق على أن كل واحد ثبت الأداة على جهازه.

والخطوة الثانية أن تعلم الفريق كتابة الاختبارات التي تمسك ما لا يمسكه الفاحص: الحالات الحدية Edge Cases، أي الصفر والقائمة الفارغة والعنصرين المتتاليين بنفس الشرط والقائمتين بطولين مختلفين، فكل خطأ صامت في هذا المقال كان سيظهر في اختبار واحد من هذا النوع. والخطوة الثالثة مراجعة الكود بقائمة تحقق قصيرة مثل التي في آخر المقال، يمر عليها المراجع في كل طلب دمج حتى تصبح عادة، ثم تقل الحاجة إليها مع الوقت. وأما ما يتعلمه المطور القادم من لغة أخرى بأسرع طريقة فهو أن يقرأ كوداً جيداً مكتوباً بـ Python، مثل كود المكتبة القياسية نفسها، فمصدر كل وحدة مرتبط في أعلى صفحة توثيقها، وقراءة `pathlib` أو `dataclasses` تعلمه الأسلوب أكثر من أي مقال.

## ما يكتبه مطور Python بدل ذلك: عادات Idioms تستحق أن تتعلمها

الأخطاء نصف الصورة، والنصف الآخر عادات صغيرة تجعل الكود يقرأ كما يتوقع أي مطور Python، وقد رأيناها كلها في مقالات هذه المرحلة:

- المرور على العناصر مباشرة `for item in cart`، ومع الرقم `enumerate(cart, start=1)`، وعلى قائمتين معاً `zip(a, b, strict=True)`، بدل `range(len(cart))` ثم الفهرس.
- التفكيك Unpacking في كل مكان: `subtotal, vat, total = order_total(lines)` و`first, *rest = items`.
- الـ Comprehensions للتحويل والتصفية البسيطة، والحلقة العادية عندما يصبح الشرط أكثر من سطر.
- `with` لكل مورد يجب أن يغلق: الملفات والاتصالات والأقفال والمعاملات، كما في `async with conn.transaction()` في خدمة الطلبات.
- `dataclass` لكائنات البيانات بدل كلاس بدالة `__init__` طويلة ودوال Getters وSetters كما في Java، ومعها `slots=True` عندما تكون كثيرة كما قسنا في مقال الذاكرة، و`@property` إذا احتجت حساباً عند القراءة.
- أسلوب EAFP مع استثناءات محددة النوع، و`raise ... from e` لكي لا يضيع السبب.
- `pathlib.Path` للمسارات بدل دمج النصوص، و`f-strings` للتنسيق.
- `is None` للمقارنة مع `None`، و`==` لكل قيمة أخرى.

## خطة القراءة Reading Plan من المصادر الرسمية

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

![أربعة أعمدة لأربعة أسابيع: الأسبوع الأول اللغة مع The Python Tutorial من الفصل 3 إلى 9 وProgramming FAQ وPEP 8 وPEP 20 وGlossary، والثاني النموذج مع Language Reference وData model وExecution model وImport system وDesign and History FAQ، والثالث المكتبة مع Built-in Functions وTypes وcollections وitertools وfunctools وpathlib وdatetime وlogging وasyncio وconcurrent.futures، والرابع المشروع مع Python Packaging User Guide وtyping وTyping Specification وHOWTOs وWhat's New in Python 3.15](https://arabroot.io/content/images/2026/10/python-idioms-05-reading-plan.webp)

أربعة أسابيع، وكل المصادر من مواقع Python الرسمية

1. الأسبوع الأول، اللغة: اقرأ [The Python Tutorial](https://docs.python.org/3/tutorial/?ref=arabroot.io) من الفصل الثالث إلى التاسع، فهو مكتوب لمن يعرف البرمجة أصلاً، وتجاوز الفصلين الأولين. ثم [Programming FAQ](https://docs.python.org/3/faq/programming.html?ref=arabroot.io) كاملة، ففيها ستة من أخطاء هذا المقال بشرح المطورين أنفسهم، ثم [PEP 8](https://peps.python.org/pep-0008/?ref=arabroot.io) و[PEP 20](https://peps.python.org/pep-0020/?ref=arabroot.io)، وأبق [المسرد Glossary](https://docs.python.org/3/glossary.html?ref=arabroot.io) مفتوحاً لكل مصطلح جديد.
2. الأسبوع الثاني، النموذج: من [Language Reference](https://docs.python.org/3/reference/?ref=arabroot.io) اقرأ فصل نموذج البيانات Data model الذي يشرح الكائنات والـ Dunder Methods، وفصل نموذج التنفيذ Execution model الذي يشرح النطاقات وسبب UnboundLocalError، و[نظام الاستيراد](https://docs.python.org/3/reference/import.html?ref=arabroot.io)، ثم [Design and History FAQ](https://docs.python.org/3/faq/design.html?ref=arabroot.io) لتعرف لماذا صممت اللغة هكذا، كالمسافة البادئة وغياب `switch` القديم.
3. الأسبوع الثالث، المكتبة القياسية: من [Library Reference](https://docs.python.org/3/library/?ref=arabroot.io) ابدأ بالدوال المدمجة والأنواع المدمجة، ثم الوحدات التي سوف تستعملها كل يوم: `collections` و`itertools` و`functools` و`pathlib` و`datetime` و`logging`، ثم `asyncio` و`concurrent.futures` مع ما قسناه في مقال التزامن.
4. الأسبوع الرابع، المشروع: [Python Packaging User Guide](https://packaging.python.org/en/latest/?ref=arabroot.io) للحزم والتوزيع، و[توثيق الأنواع ومواصفاتها](https://typing.python.org/en/latest/?ref=arabroot.io) لما بعد الأساسيات في mypy، و[HOWTOs](https://docs.python.org/3/howto/?ref=arabroot.io) وأهمها logging والترتيب Sorting والـ Descriptors، ثم [ما الجديد في Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html?ref=arabroot.io)، واجعل قراءة صفحة «ما الجديد» عادة مع كل إصدار في أكتوبر.

والطريقة التي تجعل القراءة تبقى أن تطبق كل أسبوع على شيء حقيقي: أعد كتابة جزء من خدمة الطلبات بما قرأته، أو أضف لها اختباراً لخطأ من أخطاء هذا المقال، وإذا أردت أن تذهب أبعد من الاستعمال إلى المساهمة في اللغة نفسها فـ [دليل المطورين](https://devguide.python.org/?ref=arabroot.io) هو المدخل.

## قائمة تحقق Checklist لمراجعة كود Python

- هل يوجد في سطر `def` أي قيمة افتراضية قابلة للتغيير، `[]` أو `{}` أو كائن؟
- هل تنشأ دوال أو `lambda` داخل حلقة وتستعمل متغير الحلقة؟
- هل تعدل قائمة أو قاموس أثناء المرور عليه؟
- هل يوجد `except:` أو `except Exception` في غير الطبقة العليا؟
- هل يوجد `x or default` حيث يمكن أن يكون الصفر أو النص الفارغ قيمة صحيحة؟
- هل كل `zip` على قوائم يجب أن تتساوى معه `strict=True`؟
- هل يستعمل Generator أكثر من مرة؟
- هل كل استدعاء لدالة `async` معه `await`، والتحذيرات تصل إلى السجل؟
- هل يوجد متغير باسم `list` أو `id` أو `type` أو `sum`؟
- هل قواعد ruff تشمل `B` و`A`، ويعمل mypy في CI؟
- هل الاختبارات تتحقق من القيم في الحالات الحدية: الصفر، والقائمة الفارغة، والعنصران المتتاليان؟
- هل كل التواريخ في الكود تعرف منطقتها الزمنية، والتحويل إلى توقيت السعودية يحدث عند العرض فقط؟
- هل التحذيرات `RuntimeWarning` و`DeprecationWarning` تتحول إلى فشل في الاختبارات بدل أن تضيع في stderr؟

## تدريب عملي

- شغل ruff بالقواعد `E,F,B,A,DTZ` على مشروع Python كتبته أنت، وعد كم خطأً من أخطاء هذا المقال وجد، ثم اسأل نفسك عن كل خطأ لم يجده: هل يوجد اختبار في المشروع كان سيمسكه؟
- اكتب اختباراً لخدمة الطلبات يرسل سطرين متتاليين بكمية صفر، وآخر يرسل خصماً صفراً، ولاحظ هل يحتاج الكود إلى تعديل.
- أضف إلى `test_edges.py` اختباراً للخطأ الثامن، أي `zip` بقائمتين بطولين مختلفين، وتأكد أنه يفشل على النسخة الخاطئة وينجح على المصلحة.
- جرب `ruff check --preview` على ملفات المختبر، ولاحظ هل تمسك قواعد المعاينة أخطاء أخرى من الثمانية الباقية.
- اقرأ `import this` في المفسر، واختر لكل خطأ من الخمسة عشر السطر الذي يخالفه.

## الخلاصة

وصلنا لنهاية الموضوع ونهاية المرحلة، وأهم ما فيه في نقاط قصيرة يمكنك أن تعود إليها:

- أغلب أخطاء القادمين إلى Python ليست أخطاء برمجة، وإنما افتراضات من لغة أخرى: متى تقيم القيمة الافتراضية، وماذا يحفظ الـ Closure، ومتى تبدأ دالة `async`.
- ثمانية أخطاء من خمسة عشر أعطت قيمة خاطئة برمز خروج 0، منها سلة عميل فيها قهوة عميل آخر، وخصم صفر صار 5%، ومنتج اختفى من الطلب.
- ruff بقواعد `B` و`A` أمسك ستة منها، والباقي لا يمسكه إلا اختبار يتحقق من القيم في الحالات الحدية.
- الأسلوب الـ Pythonic مجموعة قواعد تعلمها المجتمع من أخطاء حقيقية، و«يجب ألا تمر الأخطاء بصمت» تفسر نصف هذا المقال.
- كل خطأ تقريباً عادة صحيحة في لغة أخرى: القادم من JavaScript يحمل معه `||` و`let` والقيم الافتراضية التي تحسب كل مرة، والقادم من Java وC# يحمل الاستثناءات الواسعة والحلقة بالفهرس.
- اختبار واحد لكل حالة حدية، كالصفر والسطرين المتتاليين، أمسك الأخطاء الصامتة الثلاثة التي اختبرناها في أقل من عشر ثانية.
- التوثيق الرسمي يكفي لتكمل: الدرس التعليمي والأسئلة الشائعة في الأسبوع الأول، ثم مرجع اللغة، ثم المكتبة، ثم التحزيم والأنواع.

وبهذا المقال تكتمل مرحلة [لغة Python للمطورين](https://arabroot.io/start/#stage-lang-python)، والمقالات الخمسة معاً: الأدوات، والأنواع والذاكرة، والتزامن، والخدمة الحقيقية، والأخطاء الشائعة:

[![](https://arabroot.io/content/images/2026/10/python-for-devs-cover-cover.webp)دليلPython للمطورين: التركيب Syntax والأدوات في يوم واحددليل لمن يبرمج بلغة أخرى ويريد أن يبدأ بـ Python 3.15 من أول يوم: التثبيت، وما يختلف في التركيب، والأدوات uv وruff وmypy وpytest على مشروع حقيقي بمخرجه الفعلي.](https://arabroot.io/articles/python-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D8%AA%D8%B1%D9%83%D9%8A%D8%A8-%D9%88%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%81%D9%8A-%D9%8A%D9%88%D9%85-%D9%88%D8%A7%D8%AD%D8%AF/)[![](https://arabroot.io/content/images/2026/10/python-types-memory-cover-cover.webp)دليلPython من الداخل: الأنواع Types والذاكرة والاستثناءاتما هو المتغير فعلاً في Python، ومن يحرر الذاكرة، وكيف تعبر اللغة عن الخطأ، بقياسات حقيقية على Python 3.15 من حجم مليون سجل إلى Container قتلته النواة دون رسالة.](https://arabroot.io/articles/python-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AB%D9%86%D8%A7%D8%A1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/python-concurrency-cover-cover.webp)دليلالتزامن Concurrency في Python: الخيوط و asyncio وقفل GILقسنا مئة طلب HTTP وأربعة ملايين عملية هاش بالخيوط وasyncio والعمليات والبناء الخالي من القفل في Python 3.15، ثم أعدنا تسابقاً باع عشرين قطعة من مخزون عشرة وجموداً بين قفلين وأصلحناهما.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-python-%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88-asyncio-%D9%88%D9%82%D9%81%D9%84-gil/)[![](https://arabroot.io/content/images/2026/10/python-real-service-cover-cover.webp)دليلخدمة حقيقية بـ Python: FastAPI مع PostgreSQL والاختبارات Testsبنينا خدمة طلبات لمتجر صغير بـ FastAPI وPostgreSQL 17، بضريبة محسوبة بالهللة وقفل للمخزون اختبرناه بعشرين طلباً متوازياً، ثم قسنا الـ Docker Image وزمن البدء والذاكرة وwrk.](https://arabroot.io/articles/%D8%AE%D8%AF%D9%85%D8%A9-%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-%D8%AD%D9%82%D9%8A%D9%82%D9%8A%D8%A9-%D8%A8%D9%84%D8%BA%D8%A9-python-%D9%85%D8%B9-fastapi-%D9%88-postgresql/)[![](https://arabroot.io/content/images/2026/10/debugging-basics-cover-cover.webp)دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/)[![](https://arabroot.io/content/images/2026/10/polyglot-programming-cover-cover.webp)دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A8%D8%B9%D8%AF%D8%A9-%D9%84%D8%BA%D8%A7%D8%AA-polyglot-%D9%85%D8%AA%D9%89-%D8%AA%D9%81%D9%8A%D8%AF-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%B9%D9%82%D8%AF-%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتشغيل الأخطاء الخمسة عشر بالطريقتين على Python 3.15.2 في الـ Image الرسمية python:3.15.2-slim-trixie، وفحصها بـ ruff 0.17.3.