সরাসরি পর্বে যান
পর্ব ২৪ · ৫৫-এর মধ্যে · মডিউল ৪

exception — ভুল সামলানো: try, except আর raise

Python Exceptions — try, except, else, finally, raise, Chaining & EAFP

Taught in Bangla, this lesson teaches error handling in Python: what an exception is, catching only what you can handle with try/except, else and finally, raising, re-raising and chaining, the exception hierarchy and custom exceptions, EAFP versus LBYL, the limits of assert, and newer features such as exception groups and notes.

এই পর্বে যা শিখবেন

  • Exception কীভাবে একটি ফাংশন থেকে আরেকটিতে ওপরে ওঠে, আর পথে finally কখন চলে
  • try/except/else/finally-এর সঠিক ব্যবহার ও ক্রম; কেন নির্দিষ্ট exception ধরতে হয়
  • raise, আবার তোলা, raise ... from আর traceback-এর “During handling…” বার্তার মানে
  • Exception-এর বংশতালিকা, নিজের exception class, আর EAFP বনাম LBYL-এর বিচার
  • ইনপুট বারবার চেয়ে যাচাই করা, ভুল চুপচাপ গিলে না ফেলা, Python 3.11-এর নতুন সুবিধা আর AI-এর লেখা ভুল-সামলানো কোড যাচাই

১ · Exception আসলে কী — “আমি এখানে আর এগোতে পারছি না”

পর্ব ০৪-এ প্রথম লাল লেখার traceback দেখেছিলেন, আর শিখেছিলেন সেটি নিচ থেকে ওপরে পড়তে হয়। তখন এটিকে শুধু “ভুলের বার্তা” হিসেবে দেখেছিলেন। এই পর্বে প্রশ্নটি বদলাবে: ভুল হলে Python ভেতরে আসলে কী করে, আর আপনি কীভাবে সেই ভুল ধরে প্রোগ্রামকে বাঁচিয়ে রাখবেন?

একটি উদাহরণ দিয়ে শুরু করি। বন্ধুরা মিলে রেস্টুরেন্টে খেয়েছেন, বিল ৯০০ টাকা। একটি ফাংশন বিলকে মানুষের সংখ্যা দিয়ে ভাগ করে। কেউ ভুল করে মানুষের সংখ্যা ০ দিল। 900 / 0 — গণিতেই এর কোনো উত্তর নেই। Python তখন কী করবে? চুপচাপ ০ ফেরত দেবে? একটি বানানো সংখ্যা দেবে? দুটোই হতো ভয়ংকর, কারণ ভুল ফল নিয়ে প্রোগ্রাম এগিয়ে যেত আর কেউ টেরও পেত না।

Python তার বদলে একটি exception তোলে — এখানে ZeroDivisionError. Exception হলো একটি object, যার ভেতরে লেখা থাকে কী ধরনের সমস্যা, কী বার্তা, আর কোথায় ঘটেছে। এর মানে মোটামুটি এই: “এই লাইনের কাজ আমি শেষ করতে পারছি না। যে জানে কী করতে হবে, সে সামলাক।”

একটি গার্মেন্টস কারখানার কথা ভাবুন। সেলাই লাইনের একজন অপারেটর দেখলেন সুতা শেষ। তিনি নিজে গুদামে গিয়ে সুতা কিনতে পারেন না — সেই ক্ষমতা তাঁর নেই। তিনি খবরটি লাইন সুপারভাইজারকে দিলেন। সুপারভাইজারের কাছেও অতিরিক্ত সুতা নেই, তিনি জানালেন ফ্লোর ম্যানেজারকে। ম্যানেজার জানেন কী করতে হবে: গুদাম থেকে সুতা আনালেন। খবরটি নিচ থেকে ওপরে উঠেছে, যতক্ষণ না এমন একজনকে পেয়েছে যার সমাধানের ক্ষমতা আছে। আর কেউ যদি সামলাতে না পারত? তাহলে পুরো লাইন বন্ধ হয়ে যেত — প্রোগ্রামের ভাষায়, traceback দেখিয়ে প্রোগ্রাম থেমে যাওয়া।

পর্ব ১৩-এ দেখেছিলেন, ফাংশন যখন আরেকটি ফাংশনকে ডাকে, Python একটি call stack বানায় — প্রতিটি চলমান ডাকের জন্য একটি করে stack frame. Exception ঠিক এই stack ধরে ওপরে ওঠে। প্রথমে দেখে যে ফাংশনে ভুলটি ঘটল, সেখানে কোনো ধরার ব্যবস্থা আছে কি না। না থাকলে সেই ফাংশন সেখানেই শেষ — তার frame মুছে যায় — আর exception চলে যায় তাকে যে ডেকেছিল তার কাছে। একে বলে exception propagation, আর পথে frame-গুলো একে একে সরিয়ে ফেলাকে বলে stack unwinding.

নিচের প্রোগ্রামে তিনটি ফাংশন: main ডাকে checkout-কে, checkout ডাকে pay-কে। ভুল ঘটে সবচেয়ে ভেতরের pay-এ, কিন্তু সামলানো হয় সবচেয়ে বাইরের main-এ। মাঝের checkout ভুলটি ধরে না, কিন্তু পথে তার finally অংশ ঠিকই চলে (finally-এর বিস্তারিত অংশ ৩-এ)। “পরে” চাপুন:

1def pay(balance, amount):2    if amount > balance:3        raise InsufficientBalance("টাকা কম")4    return balance - amount5def checkout(total):6    try:7        return pay(300, total)8    finally:9        print("কার্ট আনলক")10def main():11    try:12        checkout(500)13    except InsufficientBalance as e:14        print("দুঃখিত:", e)15main()
Call stack আর exception-এর অবস্থান
stack
exception
এখন কী
  1. ডাকের শিকল। লাইন ১৫ থেকে main() চলল, তার try-এর ভেতরে checkout(500) ডাকা হলো। Stack-এ এখন তিনটি frame: module, main, checkout.
  2. আরও এক ধাপ ভেতরে। checkout-ও একটি try-এ ঢুকে pay(300, 500) ডাকল। চতুর্থ frame তৈরি হলো। এখনো কোনো সমস্যা নেই।
  3. ভুল ধরা পড়ল — raise. pay দেখল ৫০০ টাকা চাওয়া হয়েছে, অথচ আছে ৩০০. সে জানে না কী করতে হবে — ব্যবহারকারীকে বার্তা দেবে, নাকি অন্য অ্যাকাউন্ট থেকে নেবে? এই সিদ্ধান্ত তার কাজ নয়। তাই সে একটি InsufficientBalance exception তৈরি করে ছুড়ে দিল। লাইন ৪ আর কখনো চলবে না।
  4. pay শেষ, খবর ওপরে। pay-এর ভেতরে কোনো try নেই, তাই কেউ ধরল না। pay-এর frame মুছে গেল (stack unwinding), আর exception পৌঁছাল লাইন ৭-এ, যেখান থেকে pay ডাকা হয়েছিল। return আর হলো না।
  5. পথে finally. লাইন ৭ একটি try-এর ভেতরে, কিন্তু সেই try-এর কোনো except নেই — আছে শুধু finally. তাই checkout ভুলটি ধরবে না, কিন্তু বিদায়ের আগে finally চালাবে: কার্ট আনলক ছাপা হলো। Exception অপেক্ষা করছে; finally শেষ হতেই আবার যাত্রা শুরু।
  6. main-এ ধরা পড়ল। checkout-এর frame-ও মুছে গেল। Exception পৌঁছাল লাইন ১২-এ, যেটি main-এর try-এর ভেতরে। Python except-গুলো ওপর থেকে মিলিয়ে দেখে: InsufficientBalance? মিলেছে! Exception object-টি e নামে পাওয়া গেল।
  7. সামলানো শেষ, প্রোগ্রাম চলছে। লাইন ১৪ ব্যবহারকারীকে ভদ্র বার্তা দিল: দুঃখিত: টাকা কম. Exception এখন “সামলানো হয়ে গেছে” — প্রোগ্রাম থামেনি, কোনো traceback নেই। main-এর পরে আরও কোড থাকলে সেটি স্বাভাবিকভাবে চলত।
মূল ধারণা

Exception ভুলটি যেখানে ধরা পড়ে আর যেখানে সামলানো যায় — এই দুই জায়গাকে আলাদা করে। নিচের স্তরের ফাংশন (pay) জানে কী ভুল হয়েছে, কিন্তু জানে না কী করতে হবে। ওপরের স্তরের কোড (main) জানে ব্যবহারকারীকে কী বলতে হবে। মাঝের প্রতিটি ফাংশনে if দিয়ে ভুল পরীক্ষা করে ফেরত পাঠানোর দরকার নেই — exception নিজেই পথ খুঁজে নেয়।

কেউ না ধরলে কী হয়? Exception সবচেয়ে বাইরের module স্তর পর্যন্ত উঠে যায়, Python প্রোগ্রাম থামিয়ে দেয় আর stderr-এ traceback ছাপে। এবার পর্ব ০৪-এর সেই লাইনটির মানে নতুন করে বোঝা যায়: Traceback (most recent call last)-এর নিচের প্রতিটি File ... line ... হলো stack-এর একটি frame, exception যার ভেতর দিয়ে উঠে এসেছে। সবচেয়ে নিচের frame হলো যেখানে ভুলটি জন্মেছিল।

Exception-এর ধারণা নতুন নয়। ১৯৬০-এর দশকে IBM-এর PL/I ভাষায় “ON condition” নামে এর প্রথম দিকের রূপ ছিল। ১৯৭০-এর দশকে CLU (Barbara Liskov-এর দল, MIT) আর পরে Ada ভাষা আজকের মতো কাঠামোবদ্ধ exception জনপ্রিয় করে। C++ আর Java হয়ে ধারণাটি মূলধারায় আসে। Python প্রথম version থেকেই exception ব্যবহার করে, আর তার ডিজাইনে প্রভাব ফেলেছিল Modula-3 ভাষা — Guido van Rossum নিজেই এই ঋণের কথা লিখেছেন।

২ · try/except — যে ভুল সামলাতে জানেন, সেটিই ধরুন

Exception ধরার যন্ত্রটি হলো try/except. গঠন খুব সরল: যে কোডে ভুল হতে পারে, সেটি রাখুন try:-এর নিচে; ভুল হলে কী করবেন, সেটি রাখুন except-এর নিচে, সঙ্গে ভুলের ধরনের নাম:

def split_bill(total, people):
    try:
        return total / people
    except ZeroDivisionError:
        print("মানুষের সংখ্যা ০ হতে পারে না")
        return None

split_bill(900, 3) দিলে 300.0 ফেরত আসে, except-এর অংশ ছোঁয়াই হয় না। split_bill(900, 0) দিলে total / people লাইনে ZeroDivisionError ওঠে, Python try-এর বাকি অংশ বাদ দিয়ে সোজা মিলে যাওয়া except-এ লাফ দেয়। ভুল সামলানো হলো, প্রোগ্রাম চলতে থাকল।

কেন নির্দিষ্ট ধরন লিখতে হয়

except-এর পরে ধরনের নাম লেখা ঐচ্ছিক মনে হতে পারে। কিন্তু এটিই সবচেয়ে গুরুত্বপূর্ণ সিদ্ধান্ত। ধরুন split_bill-এ কেউ people হিসেবে "3" (string) পাঠাল। তখন আসবে TypeError, ZeroDivisionError নয়। এই ভুল ধরার কোনো except নেই, তাই সেটি ওপরে চলে যাবে — আর এটাই ঠিক. কারণ “৩ জন মানুষ string হিসেবে এসেছে” মানে কোডের অন্য কোথাও বাগ আছে; এখানে “মানুষের সংখ্যা ০” বলে বার্তা দিলে মিথ্যা বলা হতো।

নিয়ম: শুধু সেই exception ধরুন, যেটি এই জায়গায় সত্যিই সামলাতে জানেন। বাকিগুলো ওপরে যেতে দিন।

একাধিক ধরন ধরা

একই কোডে কয়েক রকম ভুল হতে পারে। দুটি উপায় আছে। প্রতিটির জন্য আলাদা আচরণ চাইলে আলাদা except লিখুন; Python ওপর থেকে নিচে মিলিয়ে দেখে, আর প্রথম যেটি মেলে শুধু সেটিই চলে. আর কয়েকটি ধরনের জন্য একই আচরণ চাইলে একটি tuple দিন:

def parse_qty(text, stock):
    try:
        qty = int(text)
        return stock[qty]
    except ValueError:
        print("সংখ্যা লিখুন")
    except (IndexError, KeyError):
        print("এই নম্বরের পণ্য নেই")

int("দুই") দিলে ValueError, তালিকার বাইরের নম্বরে IndexError, dict হলে KeyError. (Python 3.14 থেকে as না থাকলে বন্ধনী ছাড়াও except IndexError, KeyError: লেখা যায়, কিন্তু পুরোনো version-এ সেটি SyntaxError — তাই বন্ধনী দেওয়াই নিরাপদ।)

as e — exception object হাতে নেওয়া

বার্তাটি দেখাতে বা লগ করতে চাইলে except ValueError as e: লিখুন। এখন e হলো সেই exception object. str(e) বা print(e) দেয় বার্তা, type(e).__name__ দেয় ধরনের নাম। একটি সূক্ষ্ম তথ্য: except block শেষ হলেই Python নিজে থেকে e নামটি মুছে দেয়। কারণ exception-এর সাথে তার traceback আর সব frame জোড়া থাকে; নামটি রেখে দিলে সেগুলো মেমরিতে আটকে থাকত। পরে লাগলে block-এর ভেতরেই অন্য নামে রেখে দিন।

except Exception আর খালি except: — বিপজ্জনক জাল

কখনো সত্যিই “যেকোনো ভুল” ধরতে হয় — যেমন একটি ওয়েব সার্ভার, যেটি একজন ব্যবহারকারীর অনুরোধে বাগ পেলেও বাকিদের সেবা বন্ধ করবে না। তখন লেখা হয় except Exception as e:, আর অবশ্যই ভুলটি লগ করা হয়। কিন্তু bare except — অর্থাৎ ধরন ছাড়া খালি except: — আরও অনেক বেশি ধরে। সে ধরে BaseException-এর সব সন্তান, যার মধ্যে আছে:

  • KeyboardInterrupt — ব্যবহারকারী Ctrl+C চাপলে এটি ওঠে। খালি except: একে গিলে ফেললে লুপে আটকে থাকা প্রোগ্রাম আর Ctrl+C দিয়ে বন্ধ করা যায় না।
  • SystemExit — sys.exit() ডাকলে এটি ওঠে। খালি except: একে ধরলে প্রোগ্রাম বন্ধ হতে চেয়েও পারে না।
  • GeneratorExit — generator বন্ধ করার সংকেত (পর্ব ৩৪)।

এজন্য PEP 8 সরাসরি বলে: খালি except: এড়ান; যদি সত্যিই সব ধরতে হয়, অন্তত except Exception: লিখুন। আর তাও কেবল প্রোগ্রামের একেবারে বাইরের স্তরে, ভুলটি লগ করে।

৩ · else আর finally — সফলতার কাজ আর পরিষ্কারের কাজ

try-এর সাথে আরও দুটি অংশ যোগ করা যায়। পুরো গঠন, এই ক্রমেই:

  1. try: — যে কাজে ভুল হতে পারে (যত ছোট রাখা যায়, তত ভালো)
  2. except ...: — এক বা একাধিক; নির্দিষ্টগুলো আগে, সাধারণগুলো পরে
  3. else: — চলে শুধু তখন, যখন try-তে কোনো exception ওঠেনি
  4. finally: — চলে সবসময়: সফল হলেও, ধরা ভুলেও, না-ধরা ভুলেও, এমনকি try-এর ভেতরে return থাকলেও

else কেন দরকার, সব কোড try-এর ভেতরে রাখলেই তো হয়? না, কারণ তখন try বড় হয়ে যায়, আর except এমন লাইনের ভুলও ধরে ফেলতে পারে যার জন্য সেটি লেখা হয়নি। else বলে: “ভুল-প্রবণ কাজটি সফল হয়েছে; এবার বাকি কাজ — কিন্তু এর ভুল আমি এখানে ধরব না।” আর finally হলো finally — পরিষ্কারের জায়গা: ফাইল বন্ধ করা, লক ছেড়ে দেওয়া, অস্থায়ী জিনিস মুছে ফেলা।

নিচের stepper-এ একই বিল-ভাগের কোড দুবার চলবে — প্রথমে সফলভাবে (৩ জন), তারপর ভুল নিয়ে (০ জন)। কোন লাইন কখন চলে, খেয়াল করুন:

1try:2    share = total / people3except ZeroDivisionError:4    print("মানুষ ০ জন?")5else:6    print("জনপ্রতি", share)7finally:8    print("হিসাব শেষ")9print("পরের কাজ")
total = 900, people = 3
share
exception
1try:2    share = total / people3except ZeroDivisionError:4    print("মানুষ ০ জন?")5else:6    print("জনপ্রতি", share)7finally:8    print("হিসাব শেষ")9print("পরের কাজ")
total = 900, people = 0
share
exception
  1. সফল পথ — try. 900 / 3 হিসাব হলো, share = 300.0. কোনো exception ওঠেনি।
  2. except বাদ। Exception ওঠেনি, তাই except-এর block চলবে না। Python শুধু দেখে নিয়ে পরের অংশে যায়।
  3. else চলল। “ভুল হয়নি” — ঠিক এই অবস্থার জন্যই else. ফল ছাপা হলো: জনপ্রতি 300.0.
  4. finally চলল। সফল হোক বা না হোক, finally চলবেই: হিসাব শেষ.
  5. তারপর বাকি প্রোগ্রাম। পুরো try কাঠামো শেষ, লাইন ৯ চলল। সফল পথের ক্রম: try → else → finally → পরের কোড.
  6. ভুলের পথ — try. এবার people = 0. 900 / 0-তে ZeroDivisionError উঠল। share-এ কিছু assign হওয়ার আগেই লাইনটি থেমে গেছে, তাই share নামটি তৈরিই হয়নি।
  7. except মিলল। Python except ZeroDivisionError মিলিয়ে দেখল — মিলেছে। Block চলল: মানুষ ০ জন?. Exception এখন সামলানো।
  8. else বাদ, finally চলল। ভুল হয়েছিল, তাই else চলবে না — ভালোই হলো, কারণ সেখানে share ব্যবহার করলে NameError হতো। কিন্তু finally ঠিকই চলল: হিসাব শেষ.
  9. প্রোগ্রাম চলছে। ভুল সামলানো হয়েছে, তাই লাইন ৯ স্বাভাবিকভাবে চলল। ভুলের পথের ক্রম: try (মাঝপথে থামা) → except → finally → পরের কোড. কোনো except না মিললে finally চলত, তারপর exception ওপরে চলে যেত — লাইন ৯ আর চলত না।

এবার এই পর্বের প্রথম চালানো যায় এমন প্রোগ্রাম — একটি bKash-ধাঁচের টাকা পাঠানোর ফাংশন। এতে আছে নিজের তৈরি একটি exception (অংশ ৫-এ ব্যাখ্যা), raise (অংশ ৪), দুটি আলাদা except, আর else ও finally. তিনটি লেনদেনের ফল চালানোর আগে অনুমান করুন:

class InsufficientBalance(Exception):
    """ব্যালান্সের চেয়ে বেশি টাকা পাঠানোর চেষ্টা"""

def send_money(balance, amount):
    if amount <= 0:
        raise ValueError(f"অবৈধ পরিমাণ: {amount}")
    if amount > balance:
        raise InsufficientBalance(f"ব্যালান্স {balance}, চাওয়া {amount}")
    return balance - amount

for amount in (500, 5000, -20):
    try:
        left = send_money(1200, amount)
    except InsufficientBalance as e:
        print("ব্যর্থ:", e)
    except ValueError as e:
        print("ভুল ইনপুট:", e)
    else:
        print("সফল! বাকি", left)
    finally:
        print("-- লেনদেন শেষ:", amount)

আউটপুট ছয় লাইনের। ৫০০ টাকায় কোনো ভুল নেই, তাই else চলে: সফল! বাকি 700, তারপর -- লেনদেন শেষ: 500. ৫০০০ টাকায় send_money নিজে InsufficientBalance তোলে; প্রথম except মেলে: ব্যর্থ: ব্যালান্স 1200, চাওয়া 5000. −২০ টাকায় ওঠে ValueError; প্রথম except মেলে না, দ্বিতীয়টি মেলে: ভুল ইনপুট: অবৈধ পরিমাণ: -20. প্রতিবারই শেষে finally-এর লাইনটি ছাপা হয় — সফলতা আর ব্যর্থতা, দুই পথেই। খেয়াল করুন, send_money নিজে কিছু ছাপে না; সে শুধু ভুলের খবর দেয়। কী বলতে হবে তা ঠিক করে ডাকার জায়গাটি।

finally আর with — পরিষ্কারের দুই পথ

finally-এর সবচেয়ে প্রচলিত কাজ হলো কোনো সম্পদ (resource) ফেরত দেওয়া। যেমন একটি ফাইল খুললে কাজ শেষে বন্ধ করতেই হবে, মাঝপথে ভুল হলেও:

f = open("report.txt", encoding="utf-8")
try:
    data = f.read()
finally:
    f.close()          # ভুল হলেও ফাইল বন্ধ হবে

এই ধাঁচ এত বেশি লাগে যে Python এর জন্য একটি ছোট রূপ দিয়েছে — with open(...) as f:. with ভেতরে ভেতরে ঠিক এই try/finally-এর কাজ করে। একে বলে context manager. পরের পর্বে (পর্ব ২৫) ফাইলের সাথে with প্রতিদিন ব্যবহার করবেন, আর এর ভেতরের যন্ত্রপাতি দেখবেন পর্ব ৩৬-এ।

finally-এর ভেতরে return লিখবেন না। finally-তে return, break বা continue থাকলে চলমান exception চুপচাপ হারিয়ে যায় — কোনো চিহ্ন থাকে না। এটি এতই বিপজ্জনক যে Python 3.14 থেকে এমন কোডে SyntaxWarning দেখানো হয় (PEP 765). finally শুধু পরিষ্কারের জন্য; ফল ফেরত দেওয়া বা সিদ্ধান্ত নেওয়ার জন্য নয়।

৪ · raise — নিজে exception তোলা, আবার তোলা আর শিকল

এতক্ষণ Python নিজে exception তুলছিল। কিন্তু আপনার ফাংশনও বলতে পারে “এভাবে আমি কাজ করব না”. এর জন্য raise: raise ValueError("বয়স ঋণাত্মক হতে পারে না"). কখন তুলবেন? যখন ফাংশন তার প্রতিশ্রুত কাজ করতে পারছে না, আর কী করা উচিত সেই সিদ্ধান্ত তার নয়। অংশ ৩-এর send_money ঠিক এটাই করেছে।

ভালো বার্তা কেমন হয়

Exception-এর বার্তা পড়বেন আপনি নিজে, রাত দুটোয়, একটি লগ ফাইলে। তাই বার্তায় থাকা উচিত কী ভুল, কোন মানে ভুল, আর কী আশা করা হয়েছিল:

  • দুর্বল: raise ValueError("invalid") — কোন জিনিস invalid? কোন মান?
  • ভালো: raise ValueError(f"বয়স 0–119 হতে হবে, পেয়েছি {age}")

আর সঠিক ধরন বাছুন: মানটির type ঠিক কিন্তু মান অগ্রহণযোগ্য হলে ValueError; type-ই ভুল হলে TypeError; খোঁজা জিনিস না থাকলে KeyError/LookupError. সাধারণ Exception("...") তুলবেন না — তাহলে ডাকার জায়গা নির্দিষ্ট করে ধরতে পারে না।

খালি raise — দেখলাম, এবার ওপরে যাক

কখনো আপনি ভুলটি ধরতে চান শুধু কিছু একটা করার জন্য — লগ লেখা, অর্ধেক কাজ ফিরিয়ে নেওয়া — কিন্তু পুরো সামলানোর ক্ষমতা আপনার নেই। তখন except-এর ভেতরে শুধু raise লিখুন। এতে একই exception, তার আসল traceback-সহ, আবার ওপরে চলে যায়। একে বলে re-raise:

try:
    transfer(account, 5000)
except InsufficientBalance:
    print("লেনদেন বাতিল, ব্যালান্স আগের মতো")   # নিজের অংশ সেরে নিলাম
    raise                                       # বাকি সিদ্ধান্ত ওপরের কোডের

raise e না লিখে খালি raise কেন? দুটোই কাজ করে, কিন্তু খালি raise উদ্দেশ্য স্পষ্ট করে (“যা ছিল তা-ই পাঠাচ্ছি”), আর traceback-এ এই লাইনটি নতুন উৎস হিসেবে ঢুকে বিভ্রান্তি তৈরি করে না।

Exception-এর শিকল — raise ... from

আরেকটি সাধারণ পরিস্থিতি: নিচের স্তরের একটি ভুলকে ওপরের স্তরের ভাষায় অনুবাদ করা। ধরুন একটি অ্যাপের সেটিং একটি dict-এ আছে, আর "port" চাবিটি নেই। নিচের স্তরে এটি একটি KeyError, কিন্তু অ্যাপের ব্যবহারকারীর কাছে আসল কথা হলো “কনফিগারেশন অসম্পূর্ণ”. তাই আপনি KeyError ধরে নিজের ConfigError তোলেন — কিন্তু আসল কারণটি হারাতে চান না। এর জন্য exception chaining:

1def load_port(cfg):2    try:3        return int(cfg["port"])4    except KeyError as e:5        raise ConfigError("port সেটিং নেই") from e67load_port({"host": "localhost"})
নতুন exception-এর ভেতরের দুটি ঘর
__cause__
__context__
  1. নিচের ভুল। cfg["port"]-এ চাবি নেই, তাই KeyError: 'port' উঠল। except KeyError as e মিলল; e এখন সেই KeyError.
  2. অনুবাদ — from e. except-এর ভেতরে নতুন ConfigError তোলা হলো। from e লেখায় Python নতুন exception-এর __cause__ ঘরে পুরোনো KeyError রেখে দিল — “এটাই সরাসরি কারণ”. পাশাপাশি __context__ ঘরেও সেটি আছে, কারণ নতুন exception উঠেছে পুরোনোটি সামলানোর মাঝখানে।
  3. কেউ ধরল না — traceback. ConfigError module পর্যন্ত উঠে গেল। Python দুটি traceback ছাপে: আগে কারণ (KeyError), মাঝে একটি সেতু-বাক্য The above exception was the direct cause of the following exception:, তারপর নতুন ConfigError. পড়ার নিয়ম: শেষ লাইন দেখে বুঝুন কী ঘটেছে, ওপরের অংশ দেখে বুঝুন কেন।
  4. from না লিখলে। শুধু raise ConfigError(...) লিখলে __cause__ খালি থাকে, কিন্তু Python তবু __context__-এ পুরোনোটি রেখে দেয় — একে বলে implicit chaining. Traceback-এর সেতু-বাক্য তখন বদলে যায়: During handling of the above exception, another exception occurred:. এর মানে “ইচ্ছা করে অনুবাদ নয় — ভুল সামলাতে গিয়ে আরেকটি ভুল হয়েছে”. এই বার্তা দেখলে প্রায়ই আপনার except block-এই বাগ থাকে।
  5. from None — কারণ লুকানো। raise ConfigError(...) from None লিখলে Python পুরোনো exception দেখায় না, traceback-এ শুধু ConfigError. ব্যবহার করুন তখনই, যখন নিচের ভুলটি সত্যিই অপ্রাসঙ্গিক বিবরণ (যেমন একটি লাইব্রেরি ভেতরের খুঁটিনাটি লুকাতে চায়)। সন্দেহ থাকলে from e — তথ্য হারানোর চেয়ে বেশি তথ্য ভালো।

পুরো traceback দেখতে এমন (ফাইলের পথ ছোট করা হয়েছে):

Traceback (most recent call last):
  File "app.py", line 3, in load_port
    return int(cfg["port"])
               ~~~^^^^^^^^
KeyError: 'port'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "app.py", line 7, in <module>
    load_port({"host": "localhost"})
  File "app.py", line 5, in load_port
    raise ConfigError("port সেটিং নেই") from e
ConfigError: port সেটিং নেই

ছোট্ট সংযোজন — add_note: কখনো ধরন বদলানোর দরকার নেই, শুধু একটু প্রসঙ্গ যোগ করতে চান — যেমন কোন ফাইলের কোন লাইন পড়ার সময় ভুলটি হলো। Python 3.11 থেকে e.add_note("form.csv, লাইন 7") লিখে খালি raise দিলে traceback-এ মূল বার্তার ঠিক নিচে নোটটি ছাপা হয় (PEP 678). নোটগুলো জমা থাকে e.__notes__ list-এ।

৫ · Exception-এর বংশতালিকা আর নিজের exception class

Python-এর সব exception একটি বংশবৃক্ষে সাজানো। প্রতিটি exception আসলে একটি class, আর প্রতিটি class কোনো না কোনো বড় class-এর subclass (class নিয়ে বিস্তারিত পর্ব ২৯-এ)। একে বলে exception hierarchy. নিচে এর গুরুত্বপূর্ণ অংশ (পুরো তালিকা অনেক বড়):

BaseException                  ← সবার আদি; সরাসরি ধরবেন না
 ├── KeyboardInterrupt         ← Ctrl+C
 ├── SystemExit                ← sys.exit()
 ├── GeneratorExit
 └── Exception                 ← "সাধারণ" সব ভুল এর নিচে
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── ValueError
      │    └── UnicodeError
      ├── TypeError
      ├── AttributeError
      ├── NameError
      ├── OSError
      │    ├── FileNotFoundError
      │    └── PermissionError
      └── RuntimeError

এই গাছের নিয়ম একটি: except X ধরে X-কে এবং X-এর সব বংশধরকে. তাই except LookupError ধরবে IndexError আর KeyError দুটোই; except ArithmeticError ধরবে ZeroDivisionError. LookupError বা OSError-এর মতো মাঝের পরিবারগুলো কাজে লাগে যখন একটি পুরো শ্রেণির ভুলে একই আচরণ চান — যেমন ফাইল না পাওয়া, অনুমতি না থাকা বা ডিস্ক ভরে যাওয়া, সবই OSError.

সবচেয়ে নির্দিষ্টটি আগে

যেহেতু প্রথম মিলে যাওয়া except-ই চলে, সাধারণ ধরনটি ওপরে রাখলে নিচের নির্দিষ্টটি কখনো চলবে না:

try:
    price = int(text)
except Exception:          # সব ভুল এখানেই আটকে যায়
    print("কিছু একটা ভুল")
except ValueError:         # এই block কখনো চলবে না
    print("সংখ্যা লিখুন")

Python এখানে কোনো error দেয় না — কোডটি শুধু চুপচাপ ভুল আচরণ করে। নিয়ম: নির্দিষ্ট থেকে সাধারণ, ঠিক যেমন পর্ব ১১-এ কড়া শর্ত আগে রাখতে শিখেছিলেন।

নিজের exception — দুই লাইনের class

নিজের প্রোগ্রামের নিজস্ব ভুলের জন্য নিজের ধরন বানানো সবচেয়ে পরিষ্কার পথ। একে বলে custom exception. Class কী, তা পর্ব ২৯-এ শিখবেন; আপাতত এই ধাঁচটি মুখস্থ রাখলেই চলবে:

class WalletError(Exception):
    """এই ওয়ালেট অ্যাপের সব ভুলের আদি"""

class InsufficientBalance(WalletError):
    """ব্যালান্স কম"""

class DailyLimitExceeded(WalletError):
    """দিনের সীমা পার"""

তিনটি জিনিস লক্ষ করুন। প্রথমত, (Exception) লেখা মানে “এটি Exception পরিবারের সদস্য” — BaseException থেকে নয়, তাহলে সাধারণ except Exception একে ধরতে পারত না। দ্বিতীয়ত, body-তে শুধু একটি docstring — আর কিছু লাগে না; নাম আর পরিবারই আসল তথ্য। তৃতীয়ত, নাম শেষ হয় Error দিয়ে (PEP 8-এর পরামর্শ), যদিও InsufficientBalance-এর মতো স্পষ্ট নামও চলে। একটি অ্যাপের সব ভুলের একটি সাধারণ আদি (WalletError) রাখলে ব্যবহারকারী চাইলে except WalletError দিয়ে আপনার অ্যাপের সব ভুল একসাথে ধরতে পারেন, আবার চাইলে আলাদা করেও।

৬ · EAFP বনাম LBYL — অনুমতি চাওয়া, নাকি ক্ষমা চাওয়া?

ভুল সামলানোর দুটি দর্শন আছে। একটি হলো LBYL — “Look Before You Leap”, লাফ দেওয়ার আগে দেখে নাও: কাজের আগে if দিয়ে সব শর্ত পরীক্ষা করো। অন্যটি EAFP — “Easier to Ask for Forgiveness than Permission”, অনুমতি চাওয়ার চেয়ে ক্ষমা চাওয়া সহজ: কাজটি সরাসরি করো, না হলে exception ধরো। C-তে প্রথমটিই স্বাভাবিক; Python-এর অফিসিয়াল glossary দুটোকেই সংজ্ঞায়িত করে আর বলে EAFP হলো প্রচলিত Python ধাঁচ।

ঢাকার মেট্রো রেলে ঢোকার কথা ভাবুন। LBYL হলো গেটে দাঁড়িয়ে আগে কাউন্টারে গিয়ে কার্ডের ব্যালান্স জিজ্ঞেস করা, তারপর গেটে কার্ড ছোঁয়ানো। EAFP হলো সরাসরি কার্ড ছোঁয়ানো — গেট খুললে ঢুকে গেলেন; ব্যালান্স কম হলে গেট বন্ধ থাকবে আর আপনি তখন রিচার্জ করবেন। দ্বিতীয় পথে বেশিরভাগ দিন এক ধাপেই কাজ শেষ। আর প্রথম পথে একটি লুকানো ফাঁকও আছে: কাউন্টারে জিজ্ঞেস করা আর গেটে পৌঁছানোর মাঝে ব্যালান্স বদলে যেতে পারে।

সবচেয়ে প্রতিদিনের উদাহরণ — একটি সেটিং dict থেকে "port" পড়া, না থাকলে 8080. নিচে দুই দর্শন পাশাপাশি, তারপর তৃতীয় একটি পথ:

1if "port" in config:2    port = config["port"]3else:4    port = 8080
LBYL — আগে দেখে নাও
config
dict-এ খোঁজা
port
1try:2    port = config["port"]3except KeyError:4    port = 8080
EAFP — করে দেখো, না হলে সামলাও
config
dict-এ খোঁজা
port
তৃতীয় পথdict নিজেই সমাধান দেয়
port = config.get("port", 8080)
  1. LBYL — চাবি নেই। আগে প্রশ্ন: "port" in config? False. এজন্য dict-এ একবার খোঁজা হলো।
  2. LBYL — else. port = 8080. কাজ ঠিকঠাক, কোনো exception নেই।
  3. LBYL — চাবি আছে। এবার config-এ 'port': 5000 আছে। লাইন ১-এ একবার খোঁজা (in), লাইন ২-এ আবার খোঁজা ([]) — একই কাজ দুবার। আর সমস্যা শুধু গতির নয়: দুই লাইনের মাঝে অন্য কোনো thread চাবিটি মুছে দিলে লাইন ২-তে তবুও KeyError হতো। পরীক্ষা আর ব্যবহারের মাঝের এই ফাঁককে বলে race condition.
  4. EAFP — চাবি নেই। সরাসরি config["port"]. চাবি নেই, তাই KeyError উঠল, আর Python except খুঁজতে গেল।
  5. EAFP — except. except KeyError মিলল, port = 8080. ফল LBYL-এর মতোই।
  6. EAFP — চাবি আছে। এক খোঁজাতেই port = 5000, except ছোঁয়াই হলো না। সাধারণ পথটি (চাবি থাকা) সবচেয়ে সরল আর দ্রুত, আর পরীক্ষা ও ব্যবহারের মাঝে কোনো ফাঁক নেই।
  7. সবচেয়ে Pythonic — .get(). “না থাকলে একটি default” — এই কাজটি এত সাধারণ যে dict নিজেই পদ্ধতি দিয়েছে: config.get("port", 8080). এক লাইন, এক খোঁজা, কোনো exception নেই। মনে রাখুন, EAFP মানে “সব জায়গায় try লেখো” নয়; ভাষা বা লাইব্রেরি পরিষ্কার পথ দিলে সেটিই আগে।

কখন কোনটি

EAFP সবচেয়ে উজ্জ্বল যেখানে আগে থেকে পরীক্ষা করা কঠিন বা অসম্ভব। int() রূপান্তর এর সেরা উদাহরণ। একটি string পূর্ণসংখ্যা কি না, তা নিজে পরীক্ষা করতে গেলে ভাবতে হবে "-5", " 42 ", "1_000", এমনকি "৮০" — এর সবই int() গ্রহণ করে (হ্যাঁ, বাংলা অঙ্কও!)। অথচ "12.5" বা "৳80" গ্রহণ করে না। text.isdigit() দিয়ে পরীক্ষা করলে "-5" বাদ পড়বে, আর কিছু অদ্ভুত Unicode অঙ্ক পাস করে যাবে যেগুলো int() নেয় না। সবচেয়ে নির্ভরযোগ্য পরীক্ষক হলো int() নিজেই — তাই করে দেখুন, আর ValueError ধরুন।

ফাইলের ক্ষেত্রেও একই: if os.path.exists(path): open(path) লেখার মাঝে অন্য কোনো প্রোগ্রাম ফাইলটি মুছে দিতে পারে। try: open(path) আর except FileNotFoundError এই ফাঁক রাখে না। অন্যদিকে, LBYL ভালো যখন পরীক্ষাটি সস্তা আর ব্যর্থতা খুব সাধারণ ঘটনা — যেমন একটি তালিকা খালি কি না (if not items:) দেখা। আর এমন কাজের আগে, যা মাঝপথে থামলে অর্ধেক হয়ে থাকবে (দুটি অ্যাকাউন্টের একটি থেকে টাকা কেটে নেওয়ার পরে অন্যটিতে জমা দিতে না পারা), আগে যাচাই করে নেওয়াই বুদ্ধিমানের কাজ।

Exception কি ধীর? খরচের হিসাব

অনেকে C++ বা Java থেকে এসে ভাবেন exception খুব দামি। Python-এ হিসাবটি এমন: try block-এ ঢোকা প্রায় বিনামূল্যে — Python 3.11 থেকে CPython “zero-cost exception” ব্যবহার করে, অর্থাৎ ভুল না হলে try-এর জন্য কার্যত কোনো বাড়তি কাজ হয় না। খরচ হয় exception সত্যিই উঠলে — object তৈরি, traceback জোড়া, মিলে যাওয়া except খোঁজা। তবু Python-এর অন্য কাজের তুলনায় এই খরচ এত বড় নয় যে সাধারণ কোডে চিন্তা করতে হবে। ব্যবহারিক নিয়ম: যদি ব্যর্থতা বিরল হয় (বেশিরভাগ চাবি থাকে, বেশিরভাগ ইনপুট ঠিক), EAFP দ্রুত ও পরিষ্কার; যদি ব্যর্থতাই স্বাভাবিক ঘটনা হয় (অর্ধেক চাবিই নেই), .get() বা LBYL ভালো। আর সন্দেহ হলে মেপে দেখুন (পর্ব ৪৩)।

৭ · ইনপুট যাচাই, চুপচাপ গিলে ফেলা ভুল আর assert-এর সীমা

ভুল ইনপুটে আবার জিজ্ঞেস করা

পর্ব ১০-এ input() শিখেছিলেন, আর জেনেছিলেন এটি সবসময় string দেয়। ব্যবহারকারী বয়সের জায়গায় “বিশ” লিখলে int() ValueError দেবে। প্রোগ্রাম থেমে যাওয়ার বদলে ভদ্রভাবে আবার জিজ্ঞেস করাই ভালো। ধাঁচটি হলো: একটি অসীম while True লুপ, ভেতরে try, সফল হলে return বা break. এটি input validation-এর সবচেয়ে প্রচলিত রূপ।

নিচের প্রোগ্রামের দ্বিতীয় অংশে আরেকটি বাস্তব কাজ: একটি দোকানের দামের তালিকা, যেখানে কিছু দাম ঠিক, কিছু এলোমেলো। ভালোগুলো যোগ হবে, খারাপগুলো আলাদা তালিকায় জমবে — একটি খারাপ দামের জন্য পুরো হিসাব বাতিল হবে না:

def ask_age():
    while True:
        text = input("বয়স: ")
        try:
            age = int(text)
        except ValueError:
            print(f"'{text}' সংখ্যা নয়, আবার দিন")
            continue
        if 0 < age < 120:
            return age
        print(f"{age}? 1 থেকে 119-এর মধ্যে দিন")

print("ঠিক আছে:", ask_age())

prices, bad = [], []
for raw in ["120", "৮০", "45.5", "১২০ টাকা", "", "3,000"]:
    try:
        prices.append(float(raw))
    except ValueError:
        bad.append(raw)
print(sum(prices), bad)

ইনপুট হিসেবে পরপর তিনটি লাইন দেওয়া হয়েছে: বিশ, 150, 25. প্রথম লাইনে int("বিশ") ব্যর্থ, তাই ছাপা হয় 'বিশ' সংখ্যা নয়, আবার দিন. দ্বিতীয়বার 150 সংখ্যা ঠিকই, কিন্তু সীমার বাইরে — এটি exception নয়, সাধারণ if-এর কাজ: 150? 1 থেকে 119-এর মধ্যে দিন. তৃতীয়বার 25 গ্রহণযোগ্য: ঠিক আছে: 25. (আউটপুটে প্রতিটি লাইনের শুরুতে বয়স: দেখবেন, কারণ ইনপুট ফাইল থেকে আসায় আপনার টাইপ করা লেখা পর্দায় ফেরত আসে না।) শেষ লাইন 245.5 ['১২০ টাকা', '', '3,000'] — "৮০" ঠিকঠাক 80.0 হয়েছে, কারণ float() বাংলা অঙ্ক চেনে; কিন্তু সঙ্গে “টাকা” শব্দ, ফাঁকা string আর কমা-যুক্ত সংখ্যা ব্যর্থ। 120 + 80 + 45.5 = 245.5.

লক্ষ করুন, try-এর ভেতরে শুধু একটি লাইন — যেটিতে ভুল হওয়ার কথা। সীমা-পরীক্ষা বাইরে, কারণ সেটি exception নয়, একটি সাধারণ সিদ্ধান্ত। আর খারাপ দামগুলো হারিয়ে যায়নি: bad তালিকায় জমা আছে, যাতে পরে কেউ দেখে ঠিক করতে পারে।

except: pass — সবচেয়ে বিপজ্জনক দুই লাইন

এবার উল্টো ছবি। নিচের কোডটি কখনো error দেখায় না — আর ঠিক সেটাই সমস্যা:

for row in rows:
    try:
        total += float(row["price"]) * row["qty"]
    except:
        pass               # ভুল হলে? চুপ!

এই অভ্যাসকে বলে exception swallowing — ভুল গিলে ফেলা। এখানে বানান ভুল (row["prcie"]), ভুল type, এমনকি Ctrl+C — সবই নিঃশব্দে হারিয়ে যাবে। মাসের শেষে হিসাব মিলবে না, আর কোথাও কোনো সূত্র থাকবে না। ক্র্যাশ করা প্রোগ্রাম অন্তত চিৎকার করে বলে কোথায় ভুল; চুপ থাকা প্রোগ্রাম ভুল ফল দিয়ে যায়। নিয়মগুলো:

  • ভুল সামলাতে না জানলে ধরবেন না — ওপরে যেতে দিন।
  • ধরলে, অন্তত লিখে রাখুন কী হয়েছে। প্রোডাকশনে এর জন্য logging module, যেটি পর্ব ৪০-এ শিখবেন; logging.exception("...") পুরো traceback-সহ লেখে।
  • সত্যিই উপেক্ষা করার মতো নির্দিষ্ট ভুল হলে (যেমন “ফাইল আগে থেকেই নেই, মুছতে গিয়ে না পেলে সমস্যা নেই”) সেটি নাম ধরে লিখুন: contextlib.suppress(FileNotFoundError) বা except FileNotFoundError: pass — আর একটি comment দিন, কেন।

assert ইনপুট যাচাইয়ের জন্য নয়

পর্ব ১৩-এ assert দেখেছিলেন: assert amount > 0, "পরিমাণ ধনাত্মক হতে হবে". শর্ত মিথ্যা হলে AssertionError ওঠে। তাহলে ব্যবহারকারীর ইনপুট যাচাইয়ে এটি ব্যবহার করবেন না কেন? কারণ Python-কে -O (optimize) দিয়ে চালালে সব assert লাইন পুরোপুরি বাদ পড়ে যায় — যেন কখনো লেখাই হয়নি। তখন আপনার “যাচাই” আর হবে না, আর −৫০০০ টাকার লেনদেন ঢুকে পড়বে। assert হলো প্রোগ্রামারের নিজের জন্য নোট — “এটা কখনো মিথ্যা হওয়ার কথা নয়; হলে আমার কোডেই বাগ”. বাইরের ডেটা (ইনপুট, ফাইল, নেটওয়ার্ক) যাচাইয়ের জন্য if আর raise ValueError(...).

৮ · Python 3.11 আর তার পরে — একসাথে অনেক ভুল, নোট আর আরও ভালো traceback

ExceptionGroup আর except*

এতক্ষণ ধরে নিয়েছি, একবারে একটিই ভুল হয়। কিন্তু ধরুন একটি প্রোগ্রাম একসাথে দশটি ওয়েবসাইট থেকে দাম আনছে (পর্ব ৪২-এর asyncio দিয়ে)। তিনটি সাইট ব্যর্থ — একটি timeout, দুটি ভুল ডেটা। কোন ভুলটি তুলবেন? প্রথমটি তুললে বাকি দুটির খবর হারিয়ে যায়। এই সমস্যার সমাধানে Python 3.11-এ এসেছে ExceptionGroup — একটি exception, যার ভেতরে কয়েকটি exception একসাথে থাকে — আর তা ধরার নতুন লেখা except* (PEP 654):

try:
    raise ExceptionGroup("দুটি কাজ ব্যর্থ",
                         [ValueError("ভুল দাম"), TimeoutError("সাইট ধীর")])
except* ValueError as eg:
    print("ডেটার ভুল:", eg.exceptions)
except* TimeoutError as eg:
    print("সময়ের ভুল:", eg.exceptions)

সাধারণ except-এ শুধু একটি block চলে; except*-এ প্রতিটি মিলে যাওয়া block চলে, আর প্রতিটি পায় group-এর শুধু নিজের অংশটুকু। এখানে দুটো লাইনই ছাপা হবে। দৈনন্দিন স্ক্রিপ্টে এটি খুব কম লাগবে; তবে asyncio-র TaskGroup (৩.১১) ব্যবহার করলে এর সাথে দেখা হবেই।

আরও ভালো traceback — সংক্ষেপে

পর্ব ০৪-এ এর কিছু দেখেছিলেন; এখানে exception-এর দৃষ্টিতে একবার মিলিয়ে নিন:

  • Python 3.10: NameError ও AttributeError-এ “Did you mean …?” পরামর্শ, আর অনেক SyntaxError-এর বার্তা অনেক স্পষ্ট।
  • Python 3.11: লাইনের ভেতরে ঠিক কোন অংশে ভুল, তা ~~~^^^ দাগ দিয়ে দেখানো (PEP 657); add_note; ExceptionGroup; আর zero-cost try.
  • Python 3.13: টার্মিনালে রঙিন traceback.
  • Python 3.14: বন্ধনী ছাড়া except A, B: (PEP 758), আর finally-এর ভেতরে return/break/continue-এ সতর্কবার্তা (PEP 765).
C/C++ জানলে

C-তে exception নেই: ফাংশন একটি বিশেষ মান (−১, NULL) ফেরত দেয় আর errno সেট করে, আর ডাকার জায়গাকে প্রতিবার মনে করে পরীক্ষা করতে হয় — ভুলে গেলে ভুলটি নিঃশব্দে হারায়। C++-এর try/catch/throw Python-এর try/except/raise-এর খুব কাছাকাছি, কিন্তু C++-এ finally নেই — সেখানে পরিষ্কারের কাজ করে destructor (RAII), যার Python-রূপ হলো with. আরেকটি পার্থক্য: C++ সংস্কৃতিতে exception শুধু সত্যিকারের ব্যতিক্রমী অবস্থার জন্য, আর Python-এ (EAFP) এটি অনেক বেশি স্বাভাবিক — এমনকি প্রতিটি for লুপ ভেতরে ভেতরে StopIteration দিয়ে শেষ হয় (পর্ব ৩৪)। বিস্তারিত তুলনা: C++-এর exception আর std::expected.

পরীক্ষা ও ইন্টারভিউতে
  • else আর finally-এর পার্থক্য? — else চলে শুধু try-তে exception না উঠলে; finally চলে সবসময়, পরিষ্কারের কাজের জন্য।
  • খালি except: কেন খারাপ? — এটি BaseException পর্যন্ত সব ধরে, তাই KeyboardInterrupt (Ctrl+C) আর SystemExit-ও গিলে ফেলে; লিখুন নির্দিষ্ট ধরন, বা অন্তত except Exception.
  • EAFP আর LBYL কী? — EAFP: আগে কাজ করো, ব্যর্থ হলে exception ধরো (Python-এর প্রচলিত ধাঁচ); LBYL: আগে if দিয়ে পরীক্ষা করো, তারপর কাজ।
  • raise আর raise X from e-এর পার্থক্য? — খালি raise একই exception আবার ওপরে পাঠায়; from e নতুন exception তোলে আর __cause__-এ পুরোনোটি রেখে শিকল বানায়।
  • নিজের exception কীভাবে বানাবেন? — class MyError(Exception): pass — Exception থেকে, BaseException থেকে নয়।

৯ · AI-এর যুগে — ভুল-সামলানো কোডের ভুল ধরা

AI-কে “robust কোড লিখে দাও” বললে সে প্রায়ই সবকিছু try দিয়ে মুড়ে দেয়। দেখতে নিরাপদ লাগে, কিন্তু বেশিরভাগ সময় এতে ভুলগুলো শুধু লুকিয়ে যায়। কারণ AI-এর প্রশিক্ষণের কোডে “কোনোভাবে চালু রাখো” ধাঁচের উদাহরণ প্রচুর, আর এমন কোড প্রথম পরীক্ষায় কখনো ব্যর্থ হয় না — ব্যর্থ হয় পরে, চুপচাপ:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
পুরো ফাংশন try: ... except Exception: pass-এ মোড়াসব বাগ নিঃশব্দে হারায়; ভুল ফল নিয়ে প্রোগ্রাম চলতে থাকেশুধু ভুল-প্রবণ লাইন try-তে, নির্দিষ্ট ধরন, লগসহ
খালি except:Ctrl+C আর sys.exit()-ও আটকে যায়নির্দিষ্ট ধরন; বড়জোর except Exception
except Exception as e: print(e)শুধু বার্তা থাকে, ধরন আর traceback হারায়; প্রোগ্রাম ভুল অবস্থায় চলেlogging.exception() বা খালি raise
raise CustomError(str(e)) (from ছাড়া)আসল কারণ লুকায়, traceback-এ বিভ্রান্তিকর বার্তাraise ... from e
সাধারণ ঘটনায় exception দিয়ে নিয়ন্ত্রণ (লুপ ভাঙতে নিজের exception)কোড পড়া কঠিন, উদ্দেশ্য লুকায়break, return, .get(), if
ইনপুট যাচাইয়ে assert-O দিয়ে চালালে যাচাই উধাওif ...: raise ValueError(...)

AI-কে কোড লিখতে বা রিভিউ করতে বলার সময় এই শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:

আমি Python 3.12+ ব্যবহার করি। একটি মোবাইল ওয়ালেটের জন্য send_money(sender, receiver, amount) ফাংশন লিখে দাও। ব্যালান্স কম হলে, পরিমাণ শূন্য বা ঋণাত্মক হলে, আর দিনের সীমা পার হলে আলাদা আলাদা ভুল জানাতে হবে। শর্ত: ১. নিজের exception class বানাও: একটি সাধারণ WalletError (Exception থেকে), আর তার নিচে প্রতিটি নির্দিষ্ট ভুল। ২. কোথাও খালি except: বা except Exception: pass লিখবে না। যে ভুল ফাংশনটি সামলাতে জানে না, সেটি ধরবে না। ৩. try block-এ শুধু সেই লাইন রাখো, যেখানে ভুল হতে পারে; বাকি কাজ else-এ। ৪. নিচের স্তরের ভুল অনুবাদ করলে raise ... from e ব্যবহার করো। ৫. ইনপুট যাচাইয়ে assert নয়, if আর raise. ৬. ডাকার জায়গার একটি উদাহরণ দাও, যেখানে প্রতিটি ভুলের জন্য ব্যবহারকারীকে আলাদা বার্তা দেখানো হয়, আর অপ্রত্যাশিত ভুল logging.exception দিয়ে লেখা হয়। ৭. শেষে বলো: কোন কোন ভুল ইচ্ছা করে ধরা হয়নি, এবং কেন।

AI-এর লেখা কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“অনুমতি চাওয়ার চেয়ে ক্ষমা চাওয়া সহজ” — একজন নৌবাহিনীর কম্পিউটার বিজ্ঞানীর উক্তি কীভাবে Python-এর কোড লেখার রীতি হয়ে গেল? আর LBYL কি তবে ভুল?

“It's easier to ask forgiveness than it is to get permission” — এই উক্তিটি সাধারণত Grace Hopper-এর নামে প্রচলিত। Hopper ছিলেন মার্কিন নৌবাহিনীর একজন কর্মকর্তা ও কম্পিউটার বিজ্ঞানের পথিকৃৎ; প্রথম দিকের compiler-এর কাজ আর COBOL ভাষার জন্মে তাঁর বড় ভূমিকা ছিল। উক্তিটির মূল প্রসঙ্গ প্রোগ্রামিং নয়, প্রতিষ্ঠান: আমলাতান্ত্রিক কাঠামোয় প্রতিটি কাজের জন্য আগাম অনুমতি চাইতে গেলে কিছুই এগোয় না; ভালো উদ্দেশ্যে কাজটি করে ফেলা, আর দরকার হলে পরে ব্যাখ্যা দেওয়া প্রায়ই বেশি কার্যকর।

Python সম্প্রদায় এই বাক্যটির সংক্ষিপ্ত রূপ EAFP নিয়েছে, আর Python-এর অফিসিয়াল glossary-তে এটি একটি আলাদা শব্দ হিসেবে আছে — সঙ্গে বিপরীত শব্দ LBYL. Glossary নিজেই EAFP-কে বলে “common Python coding style”, এবং LBYL-এর ব্যাখ্যায় একটি গুরুত্বপূর্ণ সতর্কতা দেয়: একাধিক thread-এর পরিবেশে LBYL পদ্ধতিতে “দেখা” আর “লাফ দেওয়া”-র মাঝে race condition হতে পারে। এই পর্বের stepper-এর if key in mapping: return mapping[key] উদাহরণটিই glossary-তে আছে।

কেন এই রীতি Python-এ এত স্বাভাবিক, কিন্তু C-তে নয়? তিনটি কারণ আছে। প্রথমত, Python-এ exception সস্তা ও সর্বত্র: ভাষার নিজের কাঠামোই এর ওপর দাঁড়িয়ে — প্রতিটি for লুপ শেষ হয় StopIteration দিয়ে, hasattr() ভেতরে ভেতরে AttributeError ধরে কাজ করে। দ্বিতীয়ত, duck typing (পর্ব ০৬-এ শুনেছেন, পর্ব ৩৩-এ বিস্তারিত): “এই object-এর কি read() আছে?” আগে পরীক্ষা না করে সরাসরি ডাকা, না থাকলে exception — এটিই EAFP-এর আরেক রূপ। তৃতীয়ত, সঠিক পরীক্ষা লেখা প্রায়ই কঠিন: একটি string সংখ্যা কি না, একটি ফাইল খোলা যাবে কি না (অস্তিত্ব, অনুমতি, লক, ডিস্ক) — এগুলোর নির্ভুল আগাম পরীক্ষা লেখা মানে কাজটির পুরো যুক্তি আবার নিজে লেখা।

তবে LBYL ভুল নয়। যেখানে ব্যর্থতাই স্বাভাবিক ঘটনা, সেখানে বারবার exception তোলা ধীর আর পড়তেও অস্বাভাবিক। যেখানে কাজটির পার্শ্বফল আছে আর মাঝপথে থামলে অর্ধেক অবস্থা থেকে যায় (একটি অ্যাকাউন্ট থেকে টাকা কাটা হলো কিন্তু অন্যটিতে জমা হলো না), সেখানে আগে সব শর্ত যাচাই করাই নিরাপদ। আর ব্যবহারকারীকে সব ভুল একসাথে দেখাতে চাইলে (একটি ফর্মের পাঁচটি ঘরের সব সমস্যা) আগে থেকে যাচাই করা স্বাভাবিক।

মূল উপলব্ধি: EAFP একটি সাংস্কৃতিক পছন্দ, যা Python-এর নকশার সাথে মানানসই — সস্তা exception, duck typing আর race condition এড়ানো। কিন্তু লক্ষ্য হলো পরিষ্কার ও সঠিক কোড; কোন দর্শন সেখানে পৌঁছায়, তা পরিস্থিতি দেখে ঠিক করুন।

প্র ০২পুরোনো Python-এ string দিয়েও exception তোলা যেত — raise "ভুল". Python এটি কেন বাদ দিল, আর সব exception-কে BaseException-এর বংশধর হতে বাধ্য করল কেন?

Python-এর একেবারে শুরুর দিকে exception ছিল সাধারণ string. একটি module লিখত NotFound = "NotFound", তারপর raise NotFound, আর ধরার সময় except NotFound:. শুনতে সরল, কিন্তু এর মধ্যে বেশ কয়েকটি গভীর সমস্যা লুকিয়ে ছিল।

প্রথম সমস্যা — মেলানোর নিয়ম। String exception মেলানো হতো object-এর identity দিয়ে (is), মান দিয়ে নয়। দুটি আলাদা module যদি দুটি আলাদা "error" string বানায়, তারা মানে এক হলেও আলাদা object — একটি আরেকটিকে ধরবে না। আবার ঠিক উল্টো ঘটনাও ঘটতে পারত, কারণ ছোট string কখনো কখনো interning-এর কারণে একই object হয়ে যায় (পর্ব ০৮)। অর্থাৎ ধরা পড়বে কি না, তা নির্ভর করত interpreter-এর ভেতরের খুঁটিনাটির ওপর।

দ্বিতীয় সমস্যা — শ্রেণিবিভাগ নেই। String-এর কোনো বংশ নেই। “সব lookup ভুল ধরো” বা “এই লাইব্রেরির সব ভুল ধরো” বলার উপায় ছিল না; প্রতিটি নাম আলাদা করে লিখতে হতো। Class-এর উত্তরাধিকার ঠিক এই সমস্যার সমাধান — এই পর্বের LookupError বা WalletError-এর মতো।

তৃতীয় সমস্যা — তথ্য বহন। String শুধু একটি নাম; ভুলের সাথে বাড়তি তথ্য (কোন ফাইল, কোন error code) পাঠাতে আলাদা একটি মান সঙ্গে দিতে হতো। Class হলে exception object নিজেই সব তথ্য, পদ্ধতি আর traceback বহন করতে পারে।

তাই Python ধাপে ধাপে class-ভিত্তিক exception-এ সরে আসে। Python 1.5-এ স্ট্যান্ডার্ড লাইব্রেরির built-in exception-গুলো class হয়ে যায়। Python 2.5-এ PEP 352 সব exception-এর জন্য একটি সাধারণ আদি BaseException চালু করে, আর KeyboardInterrupt ও SystemExit-কে Exception-এর নিচ থেকে সরিয়ে সরাসরি BaseException-এর নিচে বসায় — ঠিক যাতে except Exception এগুলোকে না ধরে। একই সময়ে string exception-এ সতর্কবার্তা শুরু হয়, আর ২.৬ থেকে string তোলা বন্ধ হয়ে যায়। Python 3-এ নিয়মটি পাকা: raise-এর পরে শুধু BaseException-এর বংশধর চলে, অন্য কিছু দিলে TypeError. একই সাথে পুরোনো লেখা except ValueError, e: বদলে হয় except ValueError as e: (PEP 3110), কারণ পুরোনো রূপটি “দুটি ধরন ধরো” অর্থের সাথে গুলিয়ে যেত।

মূল উপলব্ধি: exception শুধু একটি নাম নয় — এর দরকার পরিচয় (identity নয়, type), শ্রেণিবিভাগ আর তথ্য। Class ছাড়া এই তিনটি একসাথে পাওয়া যায় না। আজকের “BaseException থেকে সবাই” নিয়মটি এক দশকের বেশি সময়ের শিক্ষার ফল।

প্র ০৩Python 2-তে একটি except-এর ভেতরে আরেকটি ভুল হলে আসল ভুলের তথ্য হারিয়ে যেত। Python 3 কীভাবে “exception chaining” দিয়ে এটি সমাধান করল?

Python 2-এ একটি বিরক্তিকর সমস্যা ছিল। ধরুন একটি ফাংশন ডেটাবেস থেকে ডেটা পড়তে গিয়ে IOError পেল, আর except-এর ভেতরে সেটি লগ করতে গিয়ে লগ-ফাইল লেখায় আরেকটি ভুল হলো। Traceback দেখাত শুধু দ্বিতীয় ভুলটি — লগ লেখার ভুল। আসল সমস্যা, ডেটাবেসের ভুল, পুরোপুরি হারিয়ে যেত। আবার ইচ্ছা করে একটি নিচের স্তরের ভুলকে ওপরের স্তরের ভুলে অনুবাদ করলেও (এই পর্বের KeyError → ConfigError) আসল কারণ ও traceback রাখার কোনো মানসম্মত উপায় ছিল না; প্রত্যেক লাইব্রেরি নিজের মতো কৌশল বানাত।

এর সমাধান PEP 3134 — “Exception Chaining and Embedded Tracebacks”, যার লেখক Ka-Ping Yee; এটি Python 3.0-তে যোগ হয়। PEP-টি exception object-এ তিনটি নতুন attribute যোগ করে:

  • __context__ — implicit chaining: একটি exception সামলানোর মাঝখানে নতুন exception উঠলে Python নিজে থেকেই পুরোনোটি এখানে রাখে। Traceback-এ সেতু-বাক্য: “During handling of the above exception, another exception occurred”.
  • __cause__ — explicit chaining: raise নতুন from পুরোনো লিখলে এখানে বসে। সেতু-বাক্য: “The above exception was the direct cause of the following exception”.
  • __traceback__ — traceback-টি exception object-এর ভেতরেই। Python 2-তে traceback পেতে sys.exc_info() ডাকতে হতো; এখন exception নিজেই তার ইতিহাস বহন করে।

দুই রকম সেতু-বাক্য রাখার সিদ্ধান্তটি সূক্ষ্ম কিন্তু খুব কাজের। “direct cause” মানে প্রোগ্রামার ইচ্ছা করে অনুবাদ করেছেন — প্রত্যাশিত নকশা। “During handling” মানে সম্ভবত দুর্ঘটনা — except block-এর ভেতরে বাগ। এক নজরে traceback দেখেই আপনি বুঝতে পারেন কোন পরিস্থিতি।

পরে আরও দুটি ছোট সংযোজন আসে। Python 3.3-এ PEP 409 চালু করে raise ... from None, যাতে লাইব্রেরি অপ্রাসঙ্গিক ভেতরের খুঁটিনাটি লুকাতে পারে; আর PEP 415 এটি বাস্তবায়নের জন্য __suppress_context__ attribute যোগ করে। এই নকশার একটি পার্শ্বফলও আছে: যেহেতু exception তার traceback ধরে রাখে, আর traceback ধরে রাখে সব frame ও তাদের local ভ্যারিয়েবল, তাই exception অনেক মেমরি আটকে রাখতে পারে। এজন্যই Python 3 except ... as e block শেষে e নামটি মুছে দেয় — অংশ ২-এ যা দেখেছেন।

মূল উপলব্ধি: ভুলের ইতিহাস হারানো ডিবাগিংয়ের সবচেয়ে বড় শত্রু। Exception chaining প্রতিটি ভুলকে তার কারণের সাথে বেঁধে রাখে — আর from e লেখা মানে ভবিষ্যতের নিজেকে একটি পূর্ণ সূত্র উপহার দেওয়া।

প্র ০৪Python 3.11-এর “zero-cost exception” আসলে কী? ভুল না হলে try-এর খরচ শূন্য হয় কীভাবে, আর ভুল হলে খরচ কোথায় যায়?

Python 3.10 পর্যন্ত CPython-এ প্রতিটি try block-এ ঢোকার সময় একটি ছোট কাজ হতো। Bytecode-এ SETUP_FINALLY ধরনের একটি নির্দেশ চলত, যা interpreter-এর একটি ভেতরের stack-এ (“block stack”) একটি নোট রাখত: “এখান থেকে ভুল হলে এই জায়গায় লাফ দিও”. try থেকে বেরোনোর সময় নোটটি সরানো হতো। ভুল না হলেও এই কাজ প্রতিবার হতো — লুপের ভেতরে try থাকলে প্রতিটি ঘুরে।

Python 3.11-এ এই পদ্ধতি বদলে যায়। এখন compiler প্রতিটি code object-এর সাথে একটি exception table বানিয়ে রাখে — একটি আলাদা তালিকা, যাতে লেখা থাকে “bytecode-এর এই পরিসরের কোনো নির্দেশে ভুল হলে এই handler-এ যাও”. try-এ ঢোকার জন্য আর কোনো নির্দেশই চলে না। তাই ভুল না হলে try-এর খরচ কার্যত শূন্য। ভুল হলে interpreter তালিকাটি দেখে ঠিক handler খুঁজে নেয়। “Zero-cost” নামটি এসেছে C++-এর একই ধরনের কৌশল থেকে, যেখানে কয়েক দশক ধরে এভাবে exception বাস্তবায়ন হয়।

এই কাজটি “Faster CPython” উদ্যোগের অংশ, যার মূল চালিকাশক্তি ছিলেন Mark Shannon; প্রকল্পে Guido van Rossum-ও যুক্ত ছিলেন, আর Microsoft এই দলের কাজে অর্থায়ন করেছিল। Python 3.11 সামগ্রিকভাবে আগের version থেকে উল্লেখযোগ্যভাবে দ্রুত হয়, আর zero-cost exception সেই উন্নতির অনেক অংশের একটি। একই version-এ frame-গুলোও হালকা করা হয় — দরকার না হলে পূর্ণ frame object তৈরিই হয় না।

তাহলে খরচ কোথায় গেল? তা পুরোপুরি চলে গেছে ভুল ঘটার পথে. Exception উঠলে Python একটি object তৈরি করে, traceback জোড়ে, তালিকা দেখে handler খোঁজে, আর stack unwinding-এর সময় প্রতিটি frame-এর পরিষ্কার করে। এগুলো সাধারণ একটি if-এর চেয়ে অনেক বেশি কাজ। এখানেই EAFP-এর হিসাব: ব্যর্থতা বিরল হলে EAFP প্রায় বিনামূল্যে, আর LBYL প্রতিবার পরীক্ষার খরচ দেয়; ব্যর্থতা প্রায়ই হলে উল্টো।

একটি সতর্কতা: এগুলো CPython-এর বাস্তবায়নের খুঁটিনাটি, ভাষার প্রতিশ্রুতি নয়। PyPy বা অন্য implementation-এ খরচের হিসাব আলাদা হতে পারে, আর ভবিষ্যতের version-এও বদলাতে পারে। তাই গতির প্রশ্নে অনুমান নয়, timeit দিয়ে মাপুন (পর্ব ৪৩)।

মূল উপলব্ধি: আধুনিক CPython-এ try লেখার কোনো দাম নেই; দাম শুধু ভুল ঘটলে। তাই নিরাপত্তার জন্য try লিখতে দ্বিধা করবেন না — শুধু exception-কে প্রতিদিনের স্বাভাবিক পথ বানাবেন না।

প্র ০৫একটি exception-ই তো যথেষ্ট ছিল — ExceptionGroup আর except*-এর মতো জটিল জিনিস Python-এ আনতে হলো কেন? এর পেছনে asyncio আর Trio-র গল্পটা কী?

Python-এর exception ব্যবস্থা একটি অনুমানের ওপর দাঁড়িয়ে ছিল: এক মুহূর্তে একটিই ভুল. একটি thread একটি লাইন চালায়, সেখানে একটি ভুল হয়, সেটি stack ধরে ওপরে ওঠে। কিন্তু concurrent প্রোগ্রামে — যেখানে একসাথে অনেক কাজ চলে — এই অনুমান ভেঙে পড়ে। দশটি network অনুরোধ একসাথে পাঠালেন, তিনটি ব্যর্থ। কোন ভুলটি তুলবেন? প্রথমটি তুললে বাকি দুটি হারায়, অথচ সেগুলো আলাদা সমস্যার খবর দিতে পারে।

এই সমস্যা সবচেয়ে স্পষ্টভাবে সামনে আনে Trio নামের একটি async লাইব্রেরি, যার স্রষ্টা Nathaniel J. Smith. Trio “structured concurrency” ধারণা জনপ্রিয় করে: একসাথে চালানো কাজগুলোকে একটি “nursery”-র ভেতরে বাঁধা থাকতে হবে, আর nursery শেষ হয় তখনই যখন সব কাজ শেষ। সব কাজের ভুল একসাথে জানাতে Trio নিজস্ব MultiError বানায়। কিন্তু ভাষার সমর্থন ছাড়া এটি ধরা ও আংশিক সামলানো ছিল অস্বস্তিকর — সাধারণ except দিয়ে “এর ভেতরের শুধু ValueError-গুলো সামলাও, বাকিগুলো ওপরে যাক” বলা যেত না।

একই সমস্যা asyncio-তেও ছিল। তাই Irit Katriel, Yury Selivanov (asyncio-র প্রধান রক্ষণাবেক্ষণকারীদের একজন) আর Guido van Rossum লেখেন PEP 654 — “Exception Groups and except*”, যা Python 3.11-এ যোগ হয়। PEP-টি আনে দুটি নতুন type — BaseExceptionGroup আর ExceptionGroup — আর নতুন লেখা except*. প্রতিটি except* group থেকে নিজের ধরনের অংশটুকু “কেটে” নেয়, একাধিক block চলতে পারে, আর কেউ না সামলানো অংশ একটি ছোট group হয়ে ওপরে চলে যায়। একই version-এ asyncio পায় TaskGroup — Trio-র nursery-র অনুপ্রেরণায় — যা ভুলগুলো ExceptionGroup হিসেবে জানায়।

নকশাটি সতর্কভাবে করা হয়েছে যাতে পুরোনো কোড না ভাঙে: সাধারণ except আগের মতোই কাজ করে, আর একই try-তে except আর except* মেশানো যায় না। তবে মূল্যও আছে — নতুন একটি লেখা, নতুন নিয়ম (যেমন except* block-এ return, break বা continue চলে না), আর একটি গাছের মতো traceback, যা পড়তে প্রথমে একটু অভ্যাস লাগে।

মূল উপলব্ধি: ভাষার মৌলিক অনুমান (“একসাথে একটি ভুল”) বদলায় যখন প্রোগ্রাম লেখার ধরন বদলায়। ExceptionGroup হলো concurrency-র যুগে exception ব্যবস্থার মানিয়ে নেওয়া — আর এর ধারণা এসেছে একটি তৃতীয় পক্ষের লাইব্রেরির বাস্তব অভিজ্ঞতা থেকে, যা Python-এর বিবর্তনের একটি পরিচিত পথ।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. ক্রম অনুমান: খাতায় লিখুন কী ছাপা হবে: try: এর নিচে print("A") আর x = int("৫"); except ValueError: এর নিচে print("B"); else: এর নিচে print("C", x); finally: এর নিচে print("D"). তারপর "৫"-এর জায়গায় "পাঁচ" দিলে কী বদলাবে?

    প্রথম ক্ষেত্রে A, C 5, D — int() বাংলা অঙ্ক “৫” চেনে, তাই কোনো exception নেই; except বাদ, else আর finally চলে। দ্বিতীয় ক্ষেত্রে A, B, D — “পাঁচ” শব্দ, অঙ্ক নয়, তাই ValueError; else চলে না, finally চলে। A দুই ক্ষেত্রেই ছাপা হয়, কারণ ভুলটি তার পরের লাইনে।

  2. কোনটি ধরবে: নিচের প্রতিটির জন্য বলুন except LookupError সেটি ধরবে কি না: (ক) [1, 2][5], (খ) {"a": 1}["b"], (গ) int("x"), (ঘ) 10 / 0.

    (ক) হ্যাঁ — IndexError, যা LookupError-এর সন্তান। (খ) হ্যাঁ — KeyError, এটিও LookupError-এর সন্তান। (গ) না — ValueError, অন্য শাখা। (ঘ) না — ZeroDivisionError, যা ArithmeticError-এর নিচে। except X ধরে X আর তার সব বংশধরকে, পাশের শাখাকে নয়।

  3. Traceback পড়া: একটি traceback-এ দুটি অংশ আর মাঝে লেখা During handling of the above exception, another exception occurred:. ওপরের অংশের শেষ লাইন FileNotFoundError: ... 'config.json', নিচের অংশের শেষ লাইন NameError: name 'logger' is not defined. কী ঘটেছিল, আর কোথায় প্রথমে হাত দেবেন?

    প্রোগ্রাম config.json খুলতে গিয়ে পায়নি — এটি প্রথম ভুল। একটি except সেটি ধরেছিল, কিন্তু সেখানে logger দিয়ে লগ লিখতে গিয়ে দেখা গেল logger নামে কিছু তৈরিই হয়নি — দ্বিতীয় ভুল, যা except block-এর নিজের বাগ। “During handling” বাক্যটিই বলে দেয় এটি ইচ্ছাকৃত অনুবাদ নয় (তাহলে লেখা থাকত “direct cause”). প্রথমে except block-এর NameError ঠিক করুন (logger তৈরি বা import), তারপর আসল সমস্যা — ফাইলটি কেন নেই বা পথ ভুল কি না।

  4. একটি ছোট ফাংশন লিখুন (এবং AI-কে যাচাই করুন): safe_average(values) লিখুন, যা string-এর একটি তালিকা নেয় (যেমন ["10", "x", "20", ""]), সংখ্যাগুলোর গড় আর খারাপ মানগুলোর তালিকা ফেরত দেয়। একটিও ভালো মান না থাকলে ValueError তুলবে, স্পষ্ট বার্তাসহ। তারপর একই কাজ কোনো AI-কে দিন, আর তার কোড এই পর্বের চেকলিস্ট দিয়ে মিলিয়ে দেখুন।

    একটি সম্ভাব্য সমাধান:

    def safe_average(values):
        good, bad = [], []
        for v in values:
            try:
                good.append(float(v))
            except ValueError:
                bad.append(v)
        if not good:
            raise ValueError(f"একটিও সংখ্যা নেই: {values!r}")
        return sum(good) / len(good), bad
    
    print(safe_average(["10", "x", "20", ""]))   # (15.0, ['x', ''])

    খেয়াল করুন: try-তে শুধু রূপান্তরের লাইন; ধরা হয়েছে শুধু ValueError — তাই কেউ তালিকায় None দিলে TypeError ওপরে যাবে, যা ঠিক, কারণ সেটি ডাকার জায়গার বাগ। খালি ফলের জন্য if not good — শূন্য দিয়ে ভাগের ZeroDivisionError হতে দেওয়ার চেয়ে স্পষ্ট বার্তার ValueError অনেক ভালো। AI যদি পুরো লুপ try/except Exception-এ মুড়ে দেয়, বা খালি তালিকায় চুপচাপ 0 ফেরত দেয়, সেটি চেকলিস্টে ধরা পড়বে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পূর্ণ try কাঠামোয় অংশগুলো যে ক্রমে লিখতে হয়, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧪 try: — ভুল হতে পারে এমন কাজ
  2. 🎯 except ValueError: — সবচেয়ে নির্দিষ্ট ধরন
  3. 🧺 except Exception: — সাধারণ ধরন, লগসহ
  4. ✅ else: — ভুল না হলে বাকি কাজ
  5. 🧹 finally: — সবসময় পরিষ্কারের কাজ

Python-এ খালি except: (কোনো ধরন ছাড়া) লিখলে নিচের কোনটিও ধরা পড়ে, যা except Exception: ধরে না?

  • ValueError
  • ZeroDivisionError
  • KeyboardInterrupt
  • KeyError

Python-এর try/except/else/finally কাঠামোয় else অংশটি কখন চলে?

  • যখন try-তে exception ওঠে আর except তা ধরে
  • শুধু যখন try-তে কোনো exception ওঠেনি
  • সবসময়, ভুল হোক বা না হোক
  • যখন exception ওঠে কিন্তু কোনো except মেলে না

একটি try-এর পরে প্রথমে except Exception: আর তারপর except ValueError: লেখা আছে। int("abc") থেকে ValueError উঠলে কী হয়?

  • except Exception-এর block চলে; ValueError-এর block কখনো চলে না
  • except ValueError-এর block চলে, কারণ সেটি বেশি নির্দিষ্ট
  • দুটি block-ই পরপর চলে
  • SyntaxError, কারণ ক্রম ভুল

Python-এ except KeyError as e:-এর ভেতরে raise ConfigError("সেটিং নেই") from e লিখলে কী ঘটে?

  • KeyError মুছে যায়, traceback-এ শুধু ConfigError থাকে
  • একই KeyError আবার ওপরে যায়
  • SyntaxError, কারণ raise-এর পরে from লেখা যায় না
  • ConfigError ওঠে, আর তার __cause__-এ KeyError সংরক্ষিত থাকে

ব্যবহারকারীর ইনপুট যাচাইয়ে assert ব্যবহার করা উচিত নয় কেন?

  • assert কোনো exception তোলে না
  • Python-কে -O দিয়ে চালালে সব assert বাদ পড়ে যায়
  • assert খুব ধীর
  • assert-এ কোনো বার্তা দেওয়া যায় না

Python-এ EAFP ধাঁচে একটি dict থেকে চাবি পড়ার উদাহরণ কোনটি?

  • if key in d: value = d[key]
  • if key in d.keys(): value = d[key]
  • try: value = d[key] / except KeyError: value = None
  • if len(d) > 0: value = d[key]

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Exception মানে “এখানে আর এগোতে পারছি না”. এটি call stack ধরে ওপরে ওঠে, পথে প্রতিটি finally চালিয়ে, যতক্ষণ না একটি মিলে যাওয়া except পায়; কেউ না ধরলে traceback দিয়ে প্রোগ্রাম থামে।
  2. নির্দিষ্ট ধরন ধরুন, নির্দিষ্ট থেকে সাধারণ ক্রমে। খালি except: নয় (Ctrl+C আর sys.exit()-ও গিলে ফেলে); except: pass দিয়ে ভুল গিলবেন না — সামলান, লগ করুন, নয়তো ওপরে যেতে দিন।
  3. ক্রম try → except → else → finally. try ছোট রাখুন; else চলে ভুল না হলে, finally সবসময় — পরিষ্কারের জন্য (with এরই ছোট রূপ)।
  4. raise দিয়ে স্পষ্ট বার্তাসহ সঠিক ধরন তুলুন; খালি raise একই ভুল আবার পাঠায়; raise ... from e কারণ রেখে শিকল বানায়। নিজের ভুলের জন্য class MyError(Exception).
  5. Python-এর রীতি EAFP — করে দেখুন, ব্যর্থ হলে ধরুন; তবে .get()-এর মতো পরিষ্কার পথ থাকলে সেটি আগে। try প্রায় বিনামূল্যে (৩.১১+), খরচ শুধু ভুল উঠলে। ইনপুট যাচাইয়ে assert নয়, if আর raise.

পরের পর্বে: প্রোগ্রামের বাইরে ডেটা রাখা — ফাইল খোলা, পড়া, লেখা; with দিয়ে নিরাপদে বন্ধ করা; আর pathlib দিয়ে Windows-Linux সবখানে চলা পথ। এই পর্বের FileNotFoundError আর finally সেখানে প্রতিদিনের সঙ্গী হবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান