2025, Oct 17 01:32
Python Enum में assert_never और MyPy बग: कारण, उदाहरण और समाधान
Python Enum में MyPy का assert_never reachability बग, उसका पुनरुत्पादन, IPython Autoreload संग व्यावहारिक सुरक्षित वर्कअराउंड और आने वाला फिक्स — संक्षेप में.
Python Enum मानों का संपूर्ण हैंडलिंग बिजनेस लॉजिक को सटीक और सुरक्षित बनाए रखने का आम तरीका है। जब टाइप चेकर समझ लेता है कि सभी केस कवर हो गए हैं, तो assert_never वाला अंतिम ब्रांच अप्राप्य होना चाहिए। लेकिन कभी-कभी स्टैटिक विश्लेषण गैर-स्पष्ट जगहों पर अटक जाता है, खासकर Enum सदस्यों पर इंस्टेंस बनाम क्लास के जरिए एट्रिब्यूट एक्सेस के आसपास।
Reproducing the issue
निम्नलिखित छोटा उदाहरण समस्या को दिखाता है। यहाँ लॉजिक एक Enum इंस्टेंस की तुलना उन सदस्यों से करता है जिन्हें उसी इंस्टेंस के जरिए एक्सेस किया गया है। अंत में else में पूर्णता जाँच के लिए assert_never है, लेकिन MyPy इसे reachable बताता है।
from enum import Enum
from typing import assert_never
class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
chosen = WeekNum(1)  # बड़े कोडबेस का सरल रूप
if chosen == chosen.MONDAY:
    print("MONDAY")
elif chosen == chosen.TUESDAY:
    print("TUESDAY")
elif chosen == chosen.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(chosen)
    # MyPy इसे पहुँच योग्य बताता है, जबकि दूसरा लिंटर इसे "Never" मानता है
What’s really happening
यह व्यवहार Enum के अर्थशास्त्र (semantics) का मुद्दा नहीं है। यह MyPy की एक बग है, जो तब टाइप नैरोइंग को प्रभावित करती है जब Enum सदस्यों तक पहुंच Enum क्लास के बजाय इंस्टेंस से की जाती है। इस स्थिति में MyPy यह नहीं मान पाता कि else असंभव है, जबकि वही लॉजिक यदि सीधे Enum क्लास का उपयोग करे तो सही तरीके से पहचाना जाता है। यही वजह है कि क्लास रेफरेंस पर स्विच करते ही वही कोड एक्सहॉस्टिवनेस चेक पास कर देता है।
Workable alternatives before the fix
आप सीधे Enum क्लास के साथ तुलना लिख सकते हैं। MyPy इस रूप को पूरी तरह exhaustive मानता है, और assert_never को अप्राप्य समझा जाता है।
from enum import Enum
from typing import assert_never
class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
picked = WeekNum(1)
if picked == WeekNum.MONDAY:
    print("MONDAY")
elif picked == WeekNum.TUESDAY:
    print("TUESDAY")
elif picked == WeekNum.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(picked)  # MyPy "Never" दिखाता है
हालाँकि, IPython में Autoreload के साथ सीधे Enum क्लास को संदर्भित करना मुश्किलें पैदा कर सकता है, क्योंकि रीलोड हुआ Enum क्लास रनटाइम इंस्टेंस के टाइप से मेल न खा सके। एक व्यवहारिक तरीका यह है कि टाइप को इंस्टेंस से ही निकाला जाए। यह MyPy की दृष्टि में एक्सहॉस्टिवनेस को संतुष्ट करता है और Autoreload के व्यवहार के प्रति अधिक सहनशील रहता है।
from enum import Enum
from typing import assert_never
class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
current = WeekNum(1)
enum_cls = type(current)
if current == enum_cls.MONDAY:
    print("MONDAY")
elif current == enum_cls.TUESDAY:
    print("TUESDAY")
elif current == enum_cls.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(current)  # MyPy "Never" दिखाता है और IPython Autoreload के साथ काम करता है
Upstream status
यह mypy की बग है। मैंने इसे कुछ दिन पहले #19422 में ठीक कर दिया है, आपका मूल स्निपेट mypy master पर काम करता है (संभवतः 1.18.0 में जारी होगा)।
दूसरे शब्दों में, जैसे ही यह फिक्स आपके पास उपलब्ध होगा, इंस्टेंस के जरिए सदस्यों से तुलना करने वाला शुरुआती पैटर्न बिना अतिरिक्त बदलावों के सही तरीके से टाइप-चेक हो जाएगा।
Why this matters
assert_never भेद्य यूनियनों और Enum टाइप्स पर exhaustive ब्रांचिंग के लिए सुरक्षा रेल की तरह काम करता है। जब टाइप चेकर गलती से इसे reachable बताता है, तो स्टैटिक विश्लेषण पर भरोसा डगमगा सकता है, जिससे अनावश्यक रिफैक्टर या नाजुक वर्कअराउंड्स जन्म लेते हैं। IPython Autoreload के साथ दोहराए जाने वाले वर्कफ़्लो में रीलोड हुई क्लासेज़ को सीधे संदर्भित करने से बचना रनटाइम/टाइप-चेकिंग के असंगतियों को भी कम करता है। यह पहचानना कि यह खास पैटर्न टूलिंग बग था, आपके कोड को डोमेन लॉजिक पर केंद्रित रखता है, न कि झूठे पॉजिटिव्स के इर्द-गिर्द।
Conclusion
यदि आप Enum इंस्टेंस की अपने ही सदस्यों से तुलना करते समय assert_never की reachability चेतावनी देखते हैं, तो मूल कारण ऊपर संदर्भित MyPy बग है। फिलहाल विकल्प के तौर पर या तो सीधे Enum क्लास से तुलना करें, या type(...) के जरिए इंस्टेंस से Enum टाइप निकालें—यह IPython Autoreload के साथ भी अनुकूल रहता है। जैसे ही PR #19422 वाला अपस्ट्रीम फिक्स उस रिलीज़ में आ जाए जो आप उपयोग करते हैं, मूल संक्षिप्त इंस्टेंस‑मेम्बर पैटर्न अपेक्षित रूप से काम करेगा। अपनी exhaustive जाँच बनाए रखें—ये वास्तविक खामियाँ पकड़ने में मदद करती हैं और टाइप-चेकिंग पाइपलाइन का सिग्नल-टू-नॉइज़ अनुपात ऊँचा रखती हैं।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Rylix) और STerliakov के उत्तर पर आधारित है।