अध्याय 24

एक्सेप्शन्स — टूटी हुई स्थिति को संभालना

try/except से विशिष्ट त्रुटि पकड़ना, except Exception: pass लगभग हमेशा गलत क्यों है, else और finally, स्वयं raise करना, और सही स्तर की त्रुटि चुनना।

36 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

पिछले अध्याय के अंतिम प्रश्न में एक प्रोग्राम ने तीन पंक्तियों को सही ढंग से संसाधित किया और चौथी पर रुक गया। छोटे रूप में यह ऐसा दिखता है:

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    total += int(text)

print(total)
text
ValueError: invalid literal for int() with base 10: 'twenty'

10 सही तरीके से जोड़ा गया था। 30 कभी नहीं जोड़ा गया। और print(total) कभी नहीं चला।

एक खराब लाइन ने पूरे काम को रोक दिया। अब तक हमारे पास केवल दो विकल्प थे — या तो डेटा का प्रत्येक हिस्सा सही हो, या प्रोग्राम रुक जाए।

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    try:
        total += int(text)
    except ValueError:
        print("skipping", text)

print(total)
text
skipping twenty
40

एक तीसरा विकल्प है: समस्या को होने दें, और फिर उसका उत्तर दें।

इस मिशन के अंत तक आप सीखेंगे

  • try / except के साथ एक विशिष्ट त्रुटि को पकड़ना
  • यह समझना कि except Exception: pass लगभग हमेशा गलत क्यों होता है
  • as err के साथ त्रुटि को पकड़ना और उसका संदेश पढ़ना
  • else और finally क्या करते हैं
  • अपनी स्वयं की त्रुटियाँ raise करना
  • त्रुटियों के परिवार (hierarchy) को पहचानना, और सही स्तर की त्रुटि पकड़ना

पूर्वापेक्षाएँ: फ़ाइलें पढ़ना और लिखना.


एक त्रुटि भी एक ऑब्जेक्ट है

as लिखने से त्रुटि आपके हाथों में आ जाती है:

python
try:
    int("twenty")
except ValueError as err:
    print(type(err))
    print(err)
text
<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'

टर्मिनल में आप जो लाल टेक्स्ट देखते रहे हैं, वह यही ऑब्जेक्ट्स हैं, और इसे प्रिंट करने पर इसका संदेश मिलता है।

सही त्रुटि को पकड़ें

python
try:
    int("twenty")
except TypeError:
    print("caught")
text
ValueError: invalid literal for int() with base 10: 'twenty'

except केवल उसी प्रकार को पकड़ता है जिसका वह नाम लेता है। इसने TypeError नाम लिया, और जो हुआ वह ValueError था — इसलिए यह नहीं रुका।

इसीलिए आपको यह कभी नहीं लिखना चाहिए:

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    try:
        total += int(text)
    except Exception:
        pass

print(total)
text
40

उत्तर सही है लेकिन कोड बुरा है। except Exception: pass सब कुछ निगल जाता है — वे विफलताएं जिनके बारे में आपने सोचा था और वे भी जिनके बारे में आपने नहीं सोचा था। जब कल कोई नाम गलत टाइप होगा, तो यह प्रोग्राम चुपचाप गलत उत्तर दे देगा।

नियम: ठीक वही त्रुटि पकड़ें जिसकी आपने योजना बनाई है।

एक से अधिक प्रकार

python
def get(data, key):
    try:
        return data[key]
    except KeyError:
        return "no such key"
    except IndexError:
        return "no such position"


print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))
text
15
no such key
no such position

जब प्रतिक्रिया समान हो तो उन्हें एक साथ लिखा जा सकता है:

python
for value in ["10", None, "x"]:
    try:
        print(int(value))
    except (ValueError, TypeError) as err:
        print(type(err).__name__, "-", err)
text
10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'

त्रुटियाँ परिवारों में आती हैं

python
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))
text
True
True
True
True

त्रुटियाँ एक वृक्ष (tree) के रूप में व्यवस्थित होती हैं, और पैरेंट को पकड़ने से उसके बच्चे भी पकड़ में आ जाते हैं:

python
try:
    raise FileNotFoundError("gone")
except OSError as err:
    print("caught as OSError:", err)
text
caught as OSError: gone

नियम: जितनी छोटी और विशिष्ट त्रुटि हो सके, उसे ही पकड़ें।

else और finally

python
def read(text):
    try:
        value = int(text)
    except ValueError:
        print("bad:", text)
        return None
    else:
        print("good:", value)
        return value
    finally:
        print("done with", text)


print(read("10"))
print(read("ten"))
text
good: 10
done with 10
10
bad: ten
done with ten
None

else तब चलता है जब try ब्लॉक में कुछ भी नहीं टूटा।

finally हमेशा चलता है, चाहे कुछ भी हुआ हो। return के बाद भी:

python
def risky():
    try:
        return "from try"
    finally:
        print("finally still runs")


print(risky())
text
finally still runs
from try

अपनी त्रुटियाँ स्वयं उठाना (raise)

त्रुटि फेंकना (raise करना) उतना ही महत्वपूर्ण है जितना पकड़ना।

python
def line_total(price, quantity):
    if quantity < 0:
        raise ValueError(f"quantity cannot be negative: {quantity}")
    return price * quantity


print(line_total(15.0, 3))
print(line_total(15.0, -1))
text
45.0
ValueError: quantity cannot be negative: -1

raise का विकल्प क्या था? None या 0 लौटाना — और तब गलती आगे बढ़ती रहती। एक त्रुटि गलती को वहीं रोक देती है जहाँ उसका जन्म हुआ था।

संदेश लिखते समय, उसमें गलत मान शामिल करें।

त्रुटि को पकड़ा भी जा सकता है और बेहतर संदेश के साथ फिर से उठाया जा सकता है:

python
def parse(text):
    try:
        return int(text)
    except ValueError:
        raise ValueError(f"not a number: {text!r}")


print(parse("10"))
print(parse("ten"))
text
10
ValueError: not a number: 'ten'

एक संपूर्ण उदाहरण

orders.txt:

text
pen,15.0,3
bag,eight,1

ink,120.0,2
clip,5.0

main.py:

python
"""एक ऑर्डर फ़ाइल पढ़ें, और किसी पंक्ति के गलत होने पर भी चलते रहें।"""

from pathlib import Path

TAX_RATE = 0.15


def parse_line(raw, number):
    """एक ऑर्डर लाइन लौटाता है, या यह बताते हुए ValueError उठाता है कि कौन सी लाइन गलत थी।"""
    parts = raw.split(",")
    if len(parts) != 3:
        raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")

    name, price, quantity = parts
    try:
        return name, float(price), int(quantity)
    except ValueError as err:
        raise ValueError(f"line {number}: {err}")


def read_order(path):
    """सही लाइनों और शिकायतों को लौटाता है, खराब डेटा के लिए कभी क्रैश नहीं होता।"""
    try:
        text = path.read_text(encoding="utf-8")
    except FileNotFoundError:
        return [], [f"no such file: {path}"]

    lines = []
    problems = []

    for number, raw in enumerate(text.splitlines(), start=1):
        if not raw.strip():
            continue
        try:
            lines.append(parse_line(raw, number))
        except ValueError as err:
            problems.append(str(err))

    return lines, problems


def main():
    lines, problems = read_order(Path("orders.txt"))

    total = 0.0
    for name, price, quantity in lines:
        amount = round(price * quantity * (1 + TAX_RATE), 2)
        total += amount
        print(f"{name:<6} {amount:>9.2f}")

    print(f"{'total':<6} {total:>9.2f}")

    print()
    print(f"{len(lines)} lines used, {len(problems)} skipped")
    for problem in problems:
        print(" -", problem)


if __name__ == "__main__":
    main()
text
pen        51.75
ink       276.00
total     327.75

2 lines used, 2 skipped
 - line 2: could not convert string to float: 'eight'
 - line 5: expected 3 fields, got 2

जब यह टूटता है

except होने के बावजूद त्रुटि नहीं पकड़ी जा रही है प्रकार मेल नहीं खाता। संदेश का पहला शब्द त्रुटि का नाम है।

except Exception: pass से उत्तर गलत आ रहे हैं इसे कोड से निकालें — यह वास्तविक बग्स को छुपा रहा है।

raise किया लेकिन कोई संदेश नहीं दिखा raise ValueError("...") के बजाय raise ValueError लिखा गया था। कोष्ठक और संदेश जोड़ें।