2025, Oct 31 07:32

mypy क्यों 'in' सदस्यता-जांच से Literal तक संकुचन नहीं करता

Python टाइपिंग में mypy सदस्यता-परीक्षण (in) से Literal संकुचन नहीं मानता; कारण, उदाहरण, pyright से अंतर और सुरक्षित समाधान जानें। उपवर्गित str केस भी देखें.

स्थिर चेकर कोड को सुरक्षित बनाते हैं, लेकिन वे सटीक नियम भी लागू करते हैं। अक्सर चकित करने वाली बात यह होती है कि जब किसी सदस्यता-जांच—जैसे किसी स्ट्रिंग का अनुमत मानों के छोटे सेट में होना—से भी mypy यह नहीं मानता कि वह मान किसी विशेष Literal है। नतीजा यह कि त्रुटि आती है, जबकि रनटाइम पर वह शाखा पूरी तरह भरोसेमंद लगती है।

न्यूनतम उदाहरण

from typing import Literal
def accept_token(tok: Literal["foo", "bar"]) -> None:
    print(f"{tok=}")
def guarded_call(raw: str) -> None:
    if raw in ["foo", "bar"]:
        accept_token(raw)
    else:
        print("format incorrect")

सदस्यता-परीक्षण मौजूद होने के बावजूद, mypy बताता है कि आर्गुमेंट का प्रकार str है और वह Literal["foo", "bar"] तक संकुचित नहीं हुआ है।

क्या हो रहा है

मुख्य कारण यह है कि mypy अभिव्यक्ति value in ["literal1", "literal2"] को प्रकार-संकुचन (type narrowing) के रूप में नहीं मानता। संरक्षित शाखा के भीतर भी, चर का प्रकार str ही रहता है। इसे डायग्नोस्टिक से आसानी से देखा जा सकता है:

def guarded_call(raw: str) -> None:
    if raw in ["foo", "bar"]:
        reveal_type(raw)  # `str`, न कि `Literal["foo", "bar"]`

इस रूप के संकुचन को समर्थन देने का अनुरोध खुला है, लेकिन आज की तारीख में mypy यह नहीं करता। संकोच केवल कार्यान्वयन की कमी नहीं है; ऐसा संकुचन सैद्धांतिक रूप से असंगत भी है। जैसे Literal['foo'] का अर्थ बिल्कुल एक ही मान होता है—ठोस स्ट्रिंग 'foo' जिसका प्रकार str है। स्ट्रिंग्स वाले कंटेनर के खिलाफ सदस्यता-जांच उन मानों के लिए भी सफल हो सकती है जो 'foo' की तरह बर्ताव करते हैं, पर वही सटीक str मान नहीं हैं—उदाहरण के लिए वे उपवर्ग जिनमें समानता और हैशिंग ओवरराइड की गई हो।

class QuasiFoo(str):
    def __eq__(self, other):
        return type(other) is str and other == "foo"
    def __hash__(self):
        return hash("foo")
print(QuasiFoo() in ["foo", "bar"])   # True
print(QuasiFoo() in ("foo", "bar"))   # True
print(QuasiFoo() in {"foo", "bar"})   # True

raw in ["foo", "bar"] जैसी जांच ऐसे मानों को स्वीकार कर सकती है, इसलिए सामान्य तौर पर उसे Literal["foo", "bar"] तक संकुचित करना सही नहीं होगा।

समाधान

वर्तमान mypy व्यवहार के मुताबिक, वह शाखा raw को Literal तक संकुचित नहीं करती, इसलिए इसे उस फ़ंक्शन में देना जो Literal["foo", "bar"] मांगता है, प्रकार-त्रुटि ट्रिगर करता है। संरक्षित ब्लॉक के भीतर प्रकार str ही रहता है, और यही डायग्नोस्टिक को समझाता है। इस रूप के संकुचन के समर्थन के लिए एक खुला मुद्दा है, पर अभी यह उपलब्ध नहीं है।

टीमों में अलग-अलग चेकर इस्तेमाल होने पर एक और बिंदु सामने आता है: pyright, literals के tuple के साथ इसी तरह संकुचन करता है, जैसे if raw in ("foo", "bar"):। यह अंतर mypy के वर्तमान व्यवहार को नहीं बदलता; यह सिर्फ दिखाता है कि प्रकार-संकुचन के नियम टूल दर टूल अलग हो सकते हैं।

यह क्यों मायने रखता है

Literal प्रकार, पूर्णता-जांच (exhaustiveness checking) और प्रकार-संकुचन सीमित प्रोटोकॉल, फ़्लैग या कॉन्फ़िगरेशन मानों को मॉडल करने में अहम होते हैं। यह मान लेना कि सदस्यता-परीक्षण से Literal की गारंटी मिलती है, आपके मानसिक मॉडल और चेकर की पाबंदियों में असंगति पैदा कर सकता है। उपवर्गीकृत स्ट्रिंग्स के साथ असंगति समझना बताता है कि mypy यहाँ सावधानी बरतता है। यह भी रेखांकित करता है कि स्थिर विश्लेषकों के बीच अंतर को जानना जरूरी है: एक टूल जो पैटर्न स्वीकार करता है, दूसरा उसे पहचान भी न पाए।

मुख्य बातें

mypy में Literal संकुचन के लिए list, tuple या set की सदस्यता पर निर्भर न रहें; संरक्षित ब्लॉक के भीतर मान str ही रहता है। यदि आपके फ़ंक्शन को सचमुच Literal चाहिए, तो अपना कोड उन प्रकार-संकुचन अभिव्यक्तियों के इर्द-गिर्द रचें जिन्हें mypy समर्थन देता है, या इस फीचर के खुले अनुरोध पर प्रगति पर नज़र रखें। यदि आप अपने वर्कफ़्लो में कई टाइप चेकर उपयोग करते हैं, तो आश्चर्य से बचने के लिए देख लें कि प्रत्येक सदस्यता-परीक्षण में Literal संकुचन को कैसे संभालता है।

यह लेख StackOverflow पर एक प्रश्न (लेखक: scnlf) और InSync के उत्तर पर आधारित है।