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

ডিবাগিং আর logging — বাগ খোঁজার বিজ্ঞান আর প্রোগ্রামের ডায়েরি

Debugging & Logging in Python — Tracebacks, pdb, breakpoint(), VS Code Debugger & the logging Module

Taught in Bangla, this lesson treats debugging as a scientific method: reading tracebacks, print debugging with f"{x=}", pdb and breakpoint(), the VS Code debugger, common Python bugs by symptom, rubber-ducking and git bisect, then replacing print with the logging module, levels, handlers, logger.exception and keeping secrets out of logs.

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

  • পুনরুৎপাদন → অনুমান → পরীক্ষা → খোঁজার জায়গা ছোট করা — এই চক্রে যেকোনো বাগ ধরা
  • Python 3.11-এর সূক্ষ্ম traceback পড়া, আর f"{x=}", repr, type দিয়ে সঠিকভাবে print-debugging
  • pdb, breakpoint(), post-mortem ডিবাগিং আর VS Code debugger-এর breakpoint, step ও watch
  • Python-এর সাতটি চেনা বাগের লক্ষণ ও দ্রুত রোগনির্ণয়; রাবার হাঁস, minimal reproducible example আর git bisect
  • logging module: level, basicConfig, ফাইলে লেখা, logger.exception, lazy formatting, logger-এর বংশতালিকা, library বনাম application-এর নিয়ম আর warnings

১ · ডিবাগিং মানে বাগের ওপর বৈজ্ঞানিক পদ্ধতি

পর্ব ০২-এ শুনেছিলেন, প্রোগ্রামের ভুলকে বলে bug, আর ভুল খুঁজে ঠিক করার কাজকে বলে debugging. পর্ব ৩৯-এ শিখেছেন pytest দিয়ে টেস্ট লিখতে — টেস্ট বলে দেয় যে কিছু একটা ভুল। কিন্তু টেস্ট লাল হলে বা ব্যবহারকারী অভিযোগ করলে প্রশ্ন আসে: ভুলটা কোথায়, আর কেন? এই পর্ব সেই প্রশ্নের উত্তর খোঁজার পদ্ধতি নিয়ে।

নতুন প্রোগ্রামাররা প্রায়ই একটি ভুল পথে হাঁটেন: কোডের দিকে তাকিয়ে থাকা, একটা কিছু বদলে চালানো, কাজ না হলে আরেকটা কিছু বদলানো। এতে কখনো কখনো বাগ চলে যায়, কিন্তু আপনি জানেন না কেন — আর প্রায়ই দুটি নতুন বাগ ঢুকে যায়। অভিজ্ঞ প্রোগ্রামাররা অন্যভাবে কাজ করেন। তাঁরা বাগটিকে একটি রহস্য হিসেবে দেখেন, আর বিজ্ঞানীর মতো প্রমাণ জোগাড় করেন।

একজন ভালো ডাক্তারের কথা ভাবুন। রোগী বললেন “পেট ব্যথা”. ডাক্তার সাথে সাথে অপারেশনের টেবিলে নেন না। আগে জিজ্ঞেস করেন কখন হয়, কী খেলে বাড়ে (লক্ষণ নিশ্চিত করা)। তারপর একটি সন্দেহ করেন — হয়তো গ্যাস্ট্রিক (অনুমান)। সেই সন্দেহ যাচাই করতে একটি নির্দিষ্ট পরীক্ষা দেন (পরীক্ষা)। ফল নেগেটিভ হলে সন্দেহের তালিকা থেকে একটি বাদ পড়ে (খোঁজার জায়গা ছোট হলো)। রোগ নিশ্চিত হলে তবেই ওষুধ। ডিবাগিং ঠিক এই রোগনির্ণয়ের কাজ — শুধু রোগীটি একটি প্রোগ্রাম।

চক্রটি চার ধাপের, আর প্রয়োজনে বারবার ঘোরে:

  1. পুনরুৎপাদন (reproduce): বাগটি ইচ্ছেমতো ঘটাতে পারেন এমন একটি নির্দিষ্ট ইনপুট খুঁজুন। যে বাগ ঘটাতে পারেন না, তা ঠিক হয়েছে কি না তাও প্রমাণ করতে পারবেন না।
  2. অনুমান (hypothesis): “আমার মনে হয়, লুপটি প্রথম জিনিসটি বাদ দিচ্ছে” — এমন একটি নির্দিষ্ট, যাচাইযোগ্য বাক্য।
  3. পরীক্ষা: অনুমানটি সত্য হলে কী দেখার কথা, তা আগে ঠিক করুন, তারপর দেখুন — print, debugger বা একটি ছোট টেস্ট দিয়ে।
  4. খোঁজার জায়গা ছোট করা: প্রতিটি পরীক্ষার পর সন্দেহের এলাকা অর্ধেক হওয়া উচিত — ঠিক পর্ব ০২-এর binary search-এর মতো।

নিচে একটি দোকানের বিলের total() ফাংশন। তিনটি জিনিসের দাম ১০০, ২০০ আর ৫০ — যোগফল হওয়ার কথা ৩৫০, কিন্তু আসছে ২৫০. কোড দেখে সাথে সাথে বাগ ধরে ফেললেও থামুন — এখানে লক্ষ্য পদ্ধতিটি দেখা, যা বড় আর অচেনা কোডেও কাজ করবে। “পরে” চাপুন:

1def total(prices):2    s = 03    for i in range(1, len(prices)):4        s += prices[i]5    return s67print(total([100, 200, 50]))   # আশা: 350
গোয়েন্দার খাতা
ধাপ
প্রমাণ
সন্দেহের এলাকা
  1. পুনরুৎপাদন। প্রথম কাজ বাগটি নিশ্চিতভাবে ঘটানো। total([100, 200, 50]) প্রতিবার ২৫০ দেয় — এখন আমাদের হাতে একটি নির্ভরযোগ্য পরীক্ষা আছে। আশা আর বাস্তবের পার্থক্য: ঠিক ১০০.
  2. ইনপুট ছোট করা। সবচেয়ে ছোট ইনপুটে কী হয়? [100] দিলে ০, [100, 200] দিলে ২০০. দুটিতেই ঠিক প্রথম দামটি হারাচ্ছে। ছোট ইনপুট বাগের চেহারা স্পষ্ট করে দেয়।
  3. অনুমান। একটি নির্দিষ্ট, যাচাইযোগ্য বাক্য: “লুপটি প্রথম জিনিস (index 0) কখনো ছোঁয় না।” অনুমান সত্য হলে লুপের ভেতরে i-এর মান কখনো 0 হবে না — পরীক্ষার আগেই আমরা জানি কী দেখার কথা।
  4. পরীক্ষা। লুপের ভেতরে একটি অস্থায়ী লাইন: print(f"{i=} {prices[i]=}"). আউটপুটে i=1 আর i=2 — i=0 নেই। অনুমান প্রমাণিত। সন্দেহ এখন শুধু লাইন ৩-এর range(1, ...).
  5. ঠিক করা। মূল কারণ: range শুরু হচ্ছে 1 থেকে। পর্ব ১২-এর পরামর্শ মনে করুন — index দরকার না হলে সরাসরি for p in prices: s += p, বা আরও সহজে return sum(prices). Index-এর হিসাবই না থাকলে off-by-one-এর সুযোগও থাকে না।
  6. আবার যাচাই ও টেস্ট। একই ইনপুটে এখন ৩৫০. অস্থায়ী print মুছে ফেলুন, আর এই বাগের জন্য একটি স্থায়ী টেস্ট লিখুন — [100, 200, 50], [100] আর খালি তালিকা []. একে বলে regression test: বাগটি ভবিষ্যতে ফিরে এলে pytest (পর্ব ৩৯) সাথে সাথে ধরবে।
মূল ধারণা

প্রতিটি পরীক্ষার আগে লিখে ফেলুন, অনুমান সত্য হলে কী দেখবেন। ফল মিললে অনুমান টিকল; না মিললে সেটিও মূল্যবান তথ্য — একটি সন্দেহ বাদ পড়ল। যে পরীক্ষার ফল আগে থেকে অনুমান করেননি, তা থেকে প্রায় কিছুই শেখা যায় না।

একটি অভ্যাস যোগ করুন: যেকোনো মুহূর্তে আপনি কী জানেন, কী অনুমান করছেন আর কী বাদ দিয়েছেন — একটি কাগজে বা টেক্সট ফাইলে লিখে রাখুন। দুই ঘণ্টা ডিবাগিংয়ের পর মাথা ক্লান্ত থাকে, আর একই পরীক্ষা দ্বিতীয়বার করা খুব সাধারণ ভুল।

২ · Traceback পড়া আর ঠিকভাবে print-debugging

প্রথম সূত্র: traceback

প্রোগ্রাম ক্র্যাশ করলে Python যে traceback দেয়, সেটিই আপনার প্রথম প্রমাণ। পর্ব ০৪-এ নিয়মটি শিখেছিলেন: নিচ থেকে ওপরে পড়ুন. শেষ লাইনে exception-এর ধরন আর বার্তা — কী ঘটেছে। তার ঠিক ওপরে যে লাইনে ঘটেছে। আরও ওপরে call stack-এর বাকি frame-গুলো — কোন পথে সেখানে পৌঁছানো হয়েছে। পর্ব ২৪-এ দেখেছেন, প্রতিটি File ... line ... একটি stack frame, যার ভেতর দিয়ে exception ওপরে উঠে এসেছে।

একটি GPA হিসাবের প্রোগ্রাম, যা ৮৫, ৭২ আর ৪০ নম্বর থেকে গড় grade point বের করে, এই traceback দিল:

Traceback (most recent call last):
  File "gpa.py", line 16, in <module>
    print(calc_gpa([85, 72, 40]))
          ~~~~~~~~^^^^^^^^^^^^^^
  File "gpa.py", line 14, in calc_gpa
    return sum(points) / len(points)
           ~~~^^^^^^^^
TypeError: unsupported operand type(s) for +: 'float' and 'NoneType'

শেষ লাইন বলছে: কেউ একটি float-এর সাথে None যোগ করতে চেয়েছে। কোথায়? লাইন ১৪-এর sum(points)-এ — ~~~^^^^^^^^ দাগটি ঠিক সেই অংশের নিচে। এটি Python 3.11-এর fine-grained error location (PEP 657): লম্বা একটি লাইনের কোন অংশে ভুল, তা দাগ দিয়ে দেখায়। আগে শুধু লাইন নম্বর পাওয়া যেত, আর a.b.c.d-এর মতো লাইনে কোনটি None তা আন্দাজ করতে হতো।

কিন্তু traceback বলে কোথায় ক্র্যাশ হয়েছে, বাগটা কোথায় জন্মেছে তা নয়। sum-এর কোনো দোষ নেই; দোষ সেই কোডের, যে points তালিকায় None ঢুকিয়েছে। এটি ডিবাগিংয়ের খুব গুরুত্বপূর্ণ পার্থক্য: লক্ষণের জায়গা আর কারণের জায়গা আলাদা হতে পারে. এখান থেকে প্রশ্ন ওঠে: None-টা কোথা থেকে এল? উত্তর খুঁজব অংশ ৩-এ, debugger দিয়ে।

print-debugging — সবচেয়ে পুরোনো, এখনো কাজের

কোডের মাঝে print বসিয়ে ভেতরের অবস্থা দেখাকে বলে print debugging. একে অনেকে “নবিশদের কৌশল” বলে ছোট করেন, কিন্তু অভিজ্ঞ প্রোগ্রামাররাও প্রতিদিন এটি ব্যবহার করেন — দ্রুত, সব জায়গায় চলে, কোনো সেটআপ লাগে না। শুধু ঠিকভাবে করতে হয়। তিনটি কৌশল:

price, qty = "120", 3
print(price, qty)            # 120 3          ← কোনটি string, বোঝা যায় না
print(f"{price=} {qty=}")    # price='120' qty=3
print(type(price), repr(price))   # <class 'str'> '120'
  • f"{x=}" (Python 3.8 থেকে): নাম আর মান একসাথে ছাপে, আর মানটি দেখায় repr রূপে। সাধারণ print(price) দেখায় 120 — সংখ্যা নাকি string, বোঝার উপায় নেই। price='120'-এর উদ্ধৃতিচিহ্ন সাথে সাথে বলে দেয়: এটি string. পাঁচটি print থাকলে কোন লাইন কোন ভ্যারিয়েবলের, সেই বিভ্রান্তিও আর থাকে না।
  • repr(): অদৃশ্য জিনিস দৃশ্যমান করে। "Rahim "-এর শেষের ফাঁকা জায়গা, "\n", বা বাংলা লেখার ভেতরের অদৃশ্য অক্ষর print-এ দেখা যায় না, repr-এ দেখা যায়।
  • type(): “এটা আসলে কী?” — অর্ধেক বাগের উত্তর এখানেই। যে জিনিসকে আপনি list ভাবছেন, সেটি হয়তো একটি tuple, একটি generator, বা None.

তবে print-debugging-এর সীমা আছে। প্রতিটি নতুন প্রশ্নের জন্য কোড বদলে আবার চালাতে হয় — প্রোগ্রাম চালাতে যদি এক মিনিট লাগে, বা বাগে পৌঁছাতে দশটি ধাপের ইনপুট দিতে হয়, তাহলে খুব ধীর। লুপের ভেতরের print হাজার লাইন ছেপে পর্দা ভাসিয়ে দেয়। আর কাজ শেষে print মুছতে ভুলে গেলে সেগুলো ব্যবহারকারীর পর্দায় বা প্রোডাকশনে চলে যায়। এই সীমাগুলোর দুটি সমাধান: থামিয়ে দেখার জন্য debugger (অংশ ৩–৪), আর স্থায়ীভাবে লিখে রাখার জন্য logging (অংশ ৭–৮)।

ছোট্ট কৌশল: অস্থায়ী debug print-এর শুরুতে একটি চিহ্ন দিন, যেমন print("DBG", f"{x=}"). কাজ শেষে Ctrl+F দিয়ে DBG খুঁজে সব মুছে ফেলতে পারবেন। আর git ব্যবহার করলে commit-এর আগে git diff দেখে নিন — ভুলে থাকা print সেখানে ধরা পড়ে।

৩ · pdb আর breakpoint() — চলন্ত প্রোগ্রামকে থামিয়ে প্রশ্ন করা

print-debugging-এ আপনাকে আগে থেকে ঠিক করতে হয় কী দেখবেন। debugger উল্টো কাজ করে: প্রোগ্রামকে একটি জায়গায় থামিয়ে দেয়, আর তখন আপনি যা খুশি জিজ্ঞেস করতে পারেন — যেকোনো ভ্যারিয়েবলের মান, যেকোনো expression, কোন ফাংশন কাকে ডেকেছে। তারপর এক লাইন এক লাইন করে এগোতে পারেন। পর্ব ০২-এ হাতে যে dry run আর trace table করেছিলেন, debugger সেটিই করে — আসল প্রোগ্রামের ওপর, নির্ভুলভাবে।

Python-এর সাথেই আসে একটি debugger: pdb (Python DeBugger), standard library-র অংশ। একে চালু করার সবচেয়ে সহজ উপায় হলো যেখানে থামতে চান, সেখানে একটি লাইন লেখা: breakpoint(). এই built-in ফাংশনটি এসেছে Python 3.7-এ (PEP 553). আগে লিখতে হতো import pdb; pdb.set_trace() — এখনো কাজ করে, কিন্তু breakpoint() ছোট, আর PYTHONBREAKPOINT=0 environment variable দিয়ে কোড না বদলেই সব breakpoint() বন্ধ করা যায়।

প্রোগ্রাম breakpoint()-এ পৌঁছালে টার্মিনালে (Pdb) prompt আসে। এখানে ছোট ছোট কমান্ড লিখে প্রোগ্রাম চালান:

কমান্ডপুরো নামকী করে
nnextএই লাইন চালিয়ে পরের লাইনে থামো; লাইনে ফাংশন ডাক থাকলে ভেতরে ঢুকো না (step over)
sstepলাইনে ফাংশন ডাক থাকলে সেই ফাংশনের ভেতরে ঢুকে থামো (step into)
rreturnএই ফাংশন শেষ হওয়া পর্যন্ত চালাও (step out-এর কাছাকাছি)
ccontinueপরের breakpoint পর্যন্ত বা শেষ পর্যন্ত চালাও
l / lllist / longlistএখনকার জায়গার আশপাশের কোড / পুরো ফাংশন দেখাও
p exprprintএকটি expression-এর মান দেখাও, যেমন p total, len(items)
pp exprpretty-printবড় dict বা list সুন্দর করে সাজিয়ে দেখাও
wwherecall stack দেখাও — কে কাকে ডেকে এখানে এসেছে
u / dup / downstack-এ এক frame ওপরে / নিচে যাও, সেখানকার ভ্যারিয়েবল দেখতে
b 13breakলাইন ১৩-এ breakpoint বসাও; b 13, m == 40 লিখলে শুধু শর্ত সত্য হলে থামবে
qquitdebugger আর প্রোগ্রাম, দুটোই বন্ধ করো

এবার অংশ ২-এর GPA বাগটি ধরি। calc_gpa-এর শুরুতে একটি breakpoint বসানো হয়েছে। প্রতিটি ধাপে ডানদিকের টার্মিনালে দেখবেন আপনি কী লিখলেন আর pdb কী উত্তর দিল (ফাইলের পথ ছোট করে দেখানো):

1def grade_point(mark):2    if mark >= 80:3        return 4.04    if mark >= 60:5        return 3.06    if mark > 40:7        return 2.089def calc_gpa(marks):10    breakpoint()11    points = []12    for m in marks:13        points.append(grade_point(m))14    return sum(points) / len(points)1516print(calc_gpa([85, 72, 40]))
এখনকার frame-এর ভ্যারিয়েবল
frame
m / mark
points
ফেরত মান
  1. থামল। সাধারণভাবে python gpa.py চালালেন। লাইন ১০-এর breakpoint()-এ পৌঁছে প্রোগ্রাম থেমে (Pdb) prompt দিল। > চিহ্নের লাইন বলছে কোন ফাইল, কোন লাইন, কোন ফাংশন; -> দেখায় পরের যে লাইনটি চলবে। (Python 3.13 থেকে pdb ঠিক breakpoint()-এর লাইনেই থামে; আগের version-এ থামত তার পরের লাইনে।)
  2. দেখা আর ফাঁদ পাতা। p marks — ইনপুট ঠিক আছে। এবার সন্দেহ: সমস্যা সম্ভবত শেষ নম্বর ৪০-এ, কারণ ৮৫ আর ৭২ সাধারণ মান। প্রতিটি ঘুরে n চাপার বদলে একটি শর্তসহ breakpoint: b 13, m == 40 — “লাইন ১৩-এ থামো, কিন্তু শুধু যখন m হলো 40”.
  3. সন্দেহভাজনের কাছে। c চাপতেই প্রোগ্রাম দুই ঘুর নিজে চলল, আর m == 40 হতেই লাইন ১৩-এ থামল। p m, points দেখাল (40, [4.0, 3.0]) — আগের দুটি grade point ঠিকঠাক। তাহলে এবারের grade_point(40) ডাকটিই সন্দেহভাজন।
  4. ফাংশনের ভেতরে — s. এখানে n চাপলে পুরো লাইন একবারে চলে যেত, ভেতরে কী হলো দেখা যেত না। তাই s (step into): pdb grade_point-এর ভেতরে ঢুকে --Call-- দেখাল। এখন frame বদলে গেছে — আমরা grade_point-এর জগতে, যেখানে mark = 40.
  5. লাইন ধরে এগোনো — n. তিনবার n: ৪০ ≥ ৮০? না। ৪০ ≥ ৬০? না। এবার লাইন ৬: mark > 40. প্রতিটি শর্ত কোন দিকে যাচ্ছে, চোখের সামনে দেখছেন — আন্দাজ নয়।
  6. অপরাধী ধরা পড়ল। আরেকবার n: --Return-- আর ->None. কোনো return চলেনি, ফাংশন শেষ পর্যন্ত গড়িয়ে গেছে, আর Python নিজে থেকে None ফেরত দিয়েছে। p mark > 40 দেখাল False — ৪০ নিজে ৪০-এর চেয়ে বড় নয়। দুটি ভুল একসাথে: >-এর জায়গায় >= হওয়ার কথা, আর কোনো শর্ত না মিললে ফেরত দেওয়ার মতো শেষ একটি return নেই।
  7. পুরো ছবি, তারপর বিদায়। w দেখাল পথটি: module → calc_gpa (লাইন ১৩) → grade_point (লাইন ৬)। চাইলে u দিয়ে calc_gpa-এর frame-এ উঠে সেখানকার ভ্যারিয়েবল দেখা যেত। q দিয়ে বেরিয়ে ঠিক করুন: if mark >= 40: return 2.0, আর শেষে return 0.0. Traceback যেখানে ক্র্যাশ দেখিয়েছিল (লাইন ১৪), বাগ ছিল সেখান থেকে অনেক দূরে।

Post-mortem — ক্র্যাশের পরে লাশ পরীক্ষা

কখনো কখনো আপনি জানেন না কোথায় breakpoint() বসাবেন — প্রোগ্রাম শুধু ক্র্যাশ করে। তখন post-mortem debugging: প্রোগ্রাম মারা যাওয়ার ঠিক মুহূর্তের অবস্থায় debugger খোলা, যখন সব ভ্যারিয়েবল তখনো মেমরিতে আছে। দুটি উপায়:

# উপায় ১: শুরু থেকেই pdb-এর অধীনে চালানো
python -m pdb gpa.py
(Pdb) c          # স্বাভাবিকভাবে চলতে দিন; ক্র্যাশ হলে pdb নিজেই থামবে

# উপায় ২: ক্র্যাশের পর interactive থেকে যাওয়া
python -i gpa.py
>>> import pdb; pdb.pm()

প্রথম উপায়ে না-ধরা exception উঠলে pdb লেখে “Uncaught exception. Entering post mortem debugging” আর ঠিক ক্র্যাশের লাইনে থামে। সেখানে p points লিখলেই দেখবেন [4.0, 3.0, None], আর u/d দিয়ে stack-এ ঘুরে দেখতে পারবেন। দ্বিতীয় উপায়ে -i flag প্রোগ্রাম শেষে (বা ক্র্যাশের পরে) REPL খোলা রাখে, আর pdb.pm() শেষ exception-এর জায়গায় debugger খোলে। Jupyter notebook-এ একই কাজ করে %debug magic.

breakpoint() কোডে রেখে commit করবেন না। সার্ভারে বা অন্য কারও মেশিনে প্রোগ্রাম চলতে চলতে হঠাৎ (Pdb) prompt-এ আটকে যাবে, আর কেউ উত্তর না দিলে চিরকাল অপেক্ষা করবে। অনেক টিম ruff-এর মতো linter দিয়ে (পর্ব ৪৪) commit-এর আগে স্বয়ংক্রিয়ভাবে ভুলে থাকা breakpoint() ধরে।

৪ · VS Code debugger — একই কাজ, চোখের সামনে

pdb সব জায়গায় চলে — সার্ভারে, SSH-এ, যেখানে কোনো গ্রাফিক্স নেই। কিন্তু নিজের কম্পিউটারে প্রতিদিনের কাজে অনেকে IDE-র গ্রাফিক্যাল debugger বেশি আরামদায়ক মনে করেন। পর্ব ০৩-এ VS Code সেটআপ করেছিলেন; Microsoft-এর Python extension-এর সাথে “Python Debugger” extension আসে, যা ভেতরে debugpy ব্যবহার করে। ধারণা হুবহু pdb-এর মতো, শুধু কমান্ডের বদলে বোতাম:

কাজকীভাবেpdb-তে সমতুল্য
Breakpoint বসানোলাইন নম্বরের বাঁয়ে ক্লিক (লাল বিন্দু), বা F9b 13
Debug শুরুF5 → “Python Debugger: Current File”python -m pdb file.py
Step OverF10n
Step IntoF11s
Step OutShift+F11r
ContinueF5c
ভ্যারিয়েবল দেখাবাঁ দিকের VARIABLES প্যানেল, বা কোডে মাউস রাখাp x
Call stackCALL STACK প্যানেল — ক্লিক করে যেকোনো frame-এ যাওয়াw, u, d

তিনটি সুবিধা আলাদা করে জানা দরকার, কারণ এগুলো সময় সবচেয়ে বেশি বাঁচায়:

  • Conditional breakpoint: লাল বিন্দুতে ডান-ক্লিক → “Edit Breakpoint” → শর্ত লিখুন, যেমন m == 40 বা len(cart) > 100. দশ হাজার সারির লুপে যে সারিতে সমস্যা, শুধু সেখানেই থামবে। “Hit Count” দিয়ে “৫০০তম বারে থামো”-ও বলা যায়।
  • Watch expression: WATCH প্যানেলে একটি expression লিখে রাখুন — sum(points), order["total"] - paid. প্রতিটি step-এর পর VS Code নিজে আবার হিসাব করে দেখায়। কোন লাইনে মানটি হঠাৎ বদলে গেল, এক নজরে চোখে পড়ে।
  • Debug Console: থেমে থাকা অবস্থায় এখানে যেকোনো Python লিখে চালাতে পারেন — এখনকার frame-এর সব ভ্যারিয়েবল হাতের কাছে। “এখানে যদি >= হতো, ফল কী হতো?” — কোড না বদলেই grade_point(40)-এর মতো পরীক্ষা চালিয়ে দেখুন।

আরেকটি সুবিধা হলো logpoint: breakpoint-এর মতোই বসান, কিন্তু থামার বদলে একটি বার্তা ছাপে, যেমন i={i} total={s}. এটি print-debugging, কিন্তু কোডে কোনো লাইন যোগ না করে — মুছতে ভোলার ভয় নেই। আর বিশেষ সেটিং (যেমন command-line argument বা environment variable) দরকার হলে .vscode/launch.json ফাইলে লিখে রাখা যায়, যাতে প্রতিবার F5 চাপলেই একইভাবে চলে।

pytest-এর সাথে: পর্ব ৩৯-এর টেস্ট ব্যর্থ হলে pytest --pdb চালান — ব্যর্থ টেস্টের ঠিক জায়গায় post-mortem pdb খুলবে। VS Code-এর Testing প্যানেলেও প্রতিটি টেস্টের পাশে “Debug Test” বোতাম আছে। ব্যর্থ টেস্ট + debugger = বাগ খোঁজার সবচেয়ে দ্রুত জুটি।

৫ · Python-এর চেনা বাগ — লক্ষণ দেখে রোগ চেনা

অভিজ্ঞ ডাক্তার অনেক রোগ লক্ষণ দেখেই সন্দেহ করেন, কারণ একই রোগ বারবার দেখেছেন। প্রোগ্রামিংয়েও তাই। Python-এ কয়েকটি বাগ এত ঘনঘন আসে যে এদের লক্ষণ মুখস্থ রাখলে অর্ধেক ডিবাগিং কয়েক সেকেন্ডে শেষ হয়:

বাগলক্ষণদ্রুত রোগনির্ণয় ও প্রতিকার
Off-by-oneএকটি জিনিস কম বা বেশি; প্রথম বা শেষটি বাদ; IndexErrorrange-এর শুরু-শেষ আর </<= দেখুন; সম্ভব হলে index ছাড়া সরাসরি for x in items
Mutable defaultআগের ডাকের ডেটা পরের ডাকে “ভূতের মতো” হাজিরdef f(x, items=[]) খুঁজুন; লিখুন items=None, তারপর ভেতরে if items is None: items = [] (পর্ব ২০)
Aliasএকটি list বদলালেন, অন্যটিও বদলে গেলb = a কপি নয়, একই object-এর দুই নাম; a is b দিয়ে যাচাই, দরকারে a.copy() বা deepcopy (পর্ব ১৯)
Built-in নাম ঢাকা (shadowing)TypeError: 'list' object is not callableআগে কোথাও list = ..., str = ..., sum = ... লেখা হয়েছে; নাম বদলান, যেমন items, total
ভুল indentationকোনো error নেই, কিন্তু একটি লাইন লুপের ভেতরে বা বাইরে চলে গেছে — ফল ভুলreturn বা যোগফলের লাইন লুপের ভেতরে কি না দেখুন; debugger-এ n চেপে দেখুন লাইনটি কতবার চলে
return ভুলে যাওয়া'NoneType' object is not subscriptable, বা None-এর সাথে যোগের TypeErrorফাংশনের সব পথে return আছে কি না দেখুন; print করে return না করাও একই বাগ; list.sort() নিজেও None দেয়
input()-এর string"5" + 1-এ TypeError; "5" * 2 দেয় "55"; "10" < "9" সত্যf"{x=}" দিয়ে উদ্ধৃতিচিহ্ন খুঁজুন; ইনপুট নেওয়ার সাথে সাথেই int()/float() (পর্ব ১০)

দুটি বাস্তব উদাহরণ ভালো করে দেখা যাক। প্রথমটি একটি অনলাইন দোকানের কার্ট — ক্লাসিক mutable default:

def add_to_cart(item, cart=[]):     # বাগ: [] একবারই তৈরি হয়
    cart.append(item)
    return cart

rahim = add_to_cart("চাল")          # ['চাল']
karim = add_to_cart("ডাল")          # ['চাল', 'ডাল']  ← করিমের কার্টে রহিমের চাল!
print(rahim is karim)               # True — দুজনের একটিই কার্ট

লক্ষণটি বিভ্রান্তিকর: একা টেস্ট করলে সব ঠিক, দ্বিতীয় গ্রাহক এলেই গোলমাল। পর্ব ২০-এ দেখেছিলেন কেন — default মান তৈরি হয় def চলার সময় একবারই, প্রতিটি ডাকে নতুন করে নয়। রোগনির্ণয়ের সূত্র হলো is: দুটি ফল একই object হলে আপনি আর অনুমান করছেন না, প্রমাণ পেয়ে গেছেন। প্রতিকার: cart=None.

দ্বিতীয়টি অংশ ৩-এর GPA বাগ, যার মূল রোগ “return ছাড়া পথ”. Python-এ যে ফাংশন কোনো return-এ পৌঁছায় না, সে চুপচাপ None ফেরত দেয় — কোনো সতর্কবার্তা নেই। আর None সাথে সাথে ক্র্যাশ করায় না; সেটি একটি তালিকায় ঢুকে বসে থাকে, আর ক্র্যাশ হয় অনেক পরে, অন্য কোনো লাইনে। এজন্যই পর্ব ৩৭-এর type hint কাজের: def grade_point(mark: int) -> float: লিখলে mypy সাবধান করে দেয় যে কোনো পথে ফাংশনটি None ফেরত দিতে পারে — প্রোগ্রাম চালানোর আগেই।

মূল ধারণা

সবচেয়ে কঠিন বাগ ক্র্যাশ করায় না — ভুল মান নিয়ে প্রোগ্রাম চলতে থাকে, আর ক্র্যাশ হয় অনেক দূরে। তাই traceback-এর লাইনকে “অপরাধের জায়গা” না ভেবে “লাশ পাওয়ার জায়গা” ভাবুন, আর জিজ্ঞেস করুন: এই ভুল মান প্রথম কোথায় জন্মেছিল?

৬ · আটকে গেলে — রাবার হাঁস, ছোট উদাহরণ আর git bisect

রাবার হাঁসকে বুঝিয়ে বলা

এক ঘণ্টা ধরে আটকে আছেন। একজন সিনিয়রকে ডেকে বোঝাতে শুরু করলেন — “এই লাইনে আমি তালিকাটা নিচ্ছি, তারপর এখানে প্রতিটি দাম… ও আচ্ছা! এখানে তো আমি index 1 থেকে শুরু করেছি।” সিনিয়র একটি কথাও বলেননি, অথচ সমস্যার সমাধান হয়ে গেল। এই অভিজ্ঞতা এত সাধারণ যে এর একটি নাম আছে: rubber duck debugging. Andrew Hunt আর David Thomas-এর The Pragmatic Programmer (১৯৯৯) বইয়ে একটি গল্প আছে — এক প্রোগ্রামার টেবিলে একটি রাবারের হাঁস রাখতেন আর আটকে গেলে হাঁসকে লাইন ধরে ধরে কোড বোঝাতেন। মাথার ভেতরে আমরা ধাপ লাফিয়ে যাই (“এখানে তো ঠিকই আছে”); মুখে বলতে গেলে প্রতিটি ধাপ স্পষ্ট করতে হয়, আর ভুল ধারণা নিজের কানেই ধরা পড়ে। হাঁস না থাকলে চায়ের কাপ, একটি খালি চেয়ার বা একটি নোটবুকও চলবে।

Minimal reproducible example — সাহায্য চাওয়ার শিল্প

নিজে না পারলে সাহায্য চাইবেন — Stack Overflow-এ, কোনো ফোরামে, সহকর্মীকে, বা AI-কে। কিন্তু “আমার কোড কাজ করছে না, এই নিন ৮০০ লাইন” লিখলে কেউ সাহায্য করতে পারে না। যা দরকার তা হলো একটি minimal reproducible example (MRE) — Stack Overflow-এর সহায়তা পাতায় এর তিনটি গুণ বলা আছে:

  • Minimal (ক্ষুদ্রতম): যত কম কোডে বাগটি দেখা যায়। ডেটাবেস, ওয়েব ফর্ম, দশটি ফাংশন — বাগ দেখাতে না লাগলে বাদ। calc_gpa([85, 72, 40])-এর বদলে শুধু grade_point(40) যদি None দেয়, সেটিই আপনার MRE.
  • Complete (সম্পূর্ণ): অন্য কেউ কপি করে সরাসরি চালাতে পারবে — দরকারি import আর ছোট নমুনা ডেটাসহ। “আমার CSV ফাইলে” নয়; ফাইলের তিনটি সারি কোডের ভেতরেই দিন।
  • Reproducible (পুনরুৎপাদনযোগ্য): চালালে সত্যিই বাগটি দেখা যায়। সঙ্গে লিখুন: কী আশা করেছিলেন, কী পেলেন, পুরো traceback, আর Python version.

মজার ব্যাপার হলো, MRE বানাতে গিয়ে প্রায় অর্ধেক সময় প্রশ্ন পোস্ট করার আগেই বাগ খুঁজে পাবেন। কোড ছেঁটে ছোট করা আসলে অংশ ১-এর “খোঁজার জায়গা ছোট করা” — প্রতিটি অংশ বাদ দিয়ে দেখছেন, বাগ থাকে কি না। অংশ ৯-এ দেখবেন, AI-কে প্রশ্ন করার সময়ও ঠিক একই নিয়ম।

git bisect — “কাল তো চলছিল!”

কিছু বাগের ক্ষেত্রে আপনি জানেন, আগের কোনো version ঠিক ছিল। গত তিন সপ্তাহে ২০০টি commit হয়েছে, কোনটি বাগ এনেছে? git bisect এর উত্তর দেয় binary search দিয়ে: আপনি একটি ভালো আর একটি খারাপ commit বলে দেন, git মাঝখানের একটি commit বের করে দেয়, আপনি পরীক্ষা করে বলেন good নাকি bad — প্রতিবার সন্দেহের তালিকা অর্ধেক হয়। ২০০টি commit-এ মাত্র ৮ বার পরীক্ষা লাগে (২৮ = ২৫৬)। আর বাগ ধরার একটি স্বয়ংক্রিয় টেস্ট থাকলে git bisect run pytest tests/test_cart.py লিখে পুরো খোঁজাটাই git-কে দিয়ে করানো যায়:

git bisect start
git bisect bad              # এখনকার version-এ বাগ আছে
git bisect good v1.4        # এই version ঠিক ছিল
# git একটি commit বের করে দেবে → পরীক্ষা করে লিখুন: git bisect good / bad
git bisect reset            # শেষে আগের জায়গায় ফেরা

৭ · logging — প্রোগ্রামের ডায়েরি, কেন print নয়

এ পর্যন্ত যা শিখলেন সবই আপনার কম্পিউটারে, আপনার চোখের সামনে ঘটা বাগের জন্য। কিন্তু ধরুন আপনার লেখা একটি পেমেন্ট সার্ভিস সার্ভারে চলছে। রাত তিনটায় একজন গ্রাহকের টাকা কেটেছে কিন্তু পেমেন্ট সফল দেখায়নি। সকালে অভিযোগ এল। তখন debugger কোনো কাজে আসবে না — ঘটনা ঘটে গেছে, আর আপনি সেখানে ছিলেন না। একমাত্র ভরসা হলো, প্রোগ্রাম চলার সময় নিজেই লিখে রেখেছিল কী কী ঘটেছে। একে বলে logging.

লঞ্চের সারেং প্রতিটি যাত্রার একটি লগবুক রাখেন: কখন ঘাট ছাড়ল, কোথায় কুয়াশা, কোন ইঞ্জিনে গোলমাল। সাধারণ দিনে কেউ সেটি পড়ে না। কিন্তু কোনো দুর্ঘটনা হলে তদন্তকারীরা প্রথমেই সেই খাতা চান — কারণ তা থেকে অনুমান না করে জানা যায় কী ঘটেছিল। প্রোগ্রামের log ঠিক সেই খাতা। “log” শব্দটিও এসেছে জাহাজের লগবুক থেকে।

তাহলে print দিয়েই তো লেখা যায়? না, কারণ বাস্তব প্রোগ্রামে print-এর চারটি বড় অভাব আছে, যা Python-এর logging module পূরণ করে:

  • গুরুত্ব (log level): প্রতিটি বার্তার একটি গুরুত্ব থাকে — সাধারণ খবর, সতর্কতা, নাকি জরুরি ভুল। print-এ সবই সমান।
  • প্রসঙ্গ: প্রতিটি লাইনে নিজে থেকেই সময়, কোন module থেকে এসেছে, কোন level — আপনাকে প্রতিবার লিখতে হয় না।
  • গন্তব্য: একই বার্তা পর্দায়, ফাইলে, বা কোনো log সার্ভারে পাঠানো যায় — শুধু কনফিগারেশন বদলে, কোড না ছুঁয়ে।
  • কমানো-বাড়ানো: সাধারণ দিনে শুধু সতর্কতা আর ভুল; সমস্যা খোঁজার দিনে এক লাইন বদলে সব খুঁটিনাটি। ডিবাগের বার্তা কোডেই থেকে যায়, মুছতে হয় না।

পাঁচটি level

Python-এর logging-এ পাঁচটি মানক level, প্রতিটির ভেতরে একটি সংখ্যা:

Levelসংখ্যাকখন ব্যবহার করবেনপেমেন্টের উদাহরণ
DEBUG10খুঁটিনাটি, শুধু সমস্যা খোঁজার সময় দরকারফাংশন কোন মান নিয়ে ডাকা হলো
INFO20স্বাভাবিক কাজের গুরুত্বপূর্ণ মাইলফলকপেমেন্ট সফল
WARNING30অপ্রত্যাশিত কিছু, কিন্তু প্রোগ্রাম সামলে নিয়েছেঅবৈধ পরিমাণ বাতিল; SMS দ্বিতীয় চেষ্টায় গেল
ERROR40একটি কাজ ব্যর্থ হয়েছেব্যালান্স কম, পেমেন্ট হয়নি
CRITICAL50পুরো প্রোগ্রাম বা সার্ভিস বিপদেডেটাবেসের সাথে যোগাযোগ বন্ধ

নিয়মটি সরল: আপনি একটি সীমা (level) ঠিক করে দেন, আর সেই সীমা বা তার চেয়ে বেশি গুরুত্বের বার্তাই দেখা যায়. সীমা WARNING হলে DEBUG আর INFO চুপচাপ বাদ পড়ে। নিচের পাঁচটি log লাইন একই রেখে শুধু সীমা বদলালে কী হয়, দেখুন:

1log.debug("cart=%s", cart)2log.info("অর্ডার 1042 হয়েছে")3log.warning("চালের স্টক কম: 2 কেজি")4log.error("SMS gateway সাড়া দিচ্ছে না")5log.critical("ডেটাবেস বন্ধ")
কনফিগারেশন
সীমা
দেখা যাবে
  1. সীমা DEBUG. সবচেয়ে নিচু সীমা, তাই পাঁচটি বার্তাই দেখা যায়। বাগ খোঁজার দিনে এটিই চাই — cart-এর ভেতরে কী ছিল পর্যন্ত। প্রতিটি লাইনের শুরুতে level আর logger-এর নাম (shop), কারণ format-এ তাই বলা আছে।
  2. সীমা INFO. DEBUG-এর সংখ্যা ১০, সীমা ২০ — তাই প্রথম লাইনটি বাদ। বাকি চারটি থাকে। অনেক ছোট অ্যাপের স্বাভাবিক সেটিং এটি।
  3. সীমা WARNING. এবার শুধু “কিছু একটা ঠিক নেই” ধরনের বার্তা। প্রোডাকশনের ব্যস্ত সার্ভারে প্রায়ই এই সীমা রাখা হয়, যাতে প্রতিটি সফল অর্ডারের লাইন ডিস্ক ভরিয়ে না ফেলে। খেয়াল করুন, কোডের একটি লাইনও বদলায়নি।
  4. সীমা ERROR. শুধু ব্যর্থতা আর বিপদ। কোনো alerting ব্যবস্থা (যেমন ERROR এলে on-call ইঞ্জিনিয়ারের ফোনে বার্তা) প্রায়ই এই সীমা দেখে।
  5. কিছুই কনফিগার না করলে। basicConfig না ডাকলে root logger-এর সীমা থাকে WARNING, তাই এখানেও তিনটি বার্তা আসে — কিন্তু চেহারা আলাদা: শুধু বার্তা, level বা নাম ছাড়া, আর যায় stderr-এ। এটি logging-এর “শেষ ভরসা” handler (logging.lastResort), যাতে কনফিগারেশন ভুলে গেলেও জরুরি খবর হারিয়ে না যায়। এজন্যই অনেকে অবাক হন: “আমার log.info() কিছুই ছাপছে না কেন?”

প্রথম logging প্রোগ্রাম — bKash-ধাঁচের পেমেন্ট

তিনটি জিনিস দিয়ে শুরু: logging.basicConfig() দিয়ে একবার কনফিগারেশন, logging.getLogger(নাম) দিয়ে একটি logger নেওয়া, আর সেই logger-এর .debug(), .info(), .warning(), .error() ডাকা। চালানোর আগে অনুমান করুন — চারটি log ডাকের মধ্যে কোনগুলো ছাপা হবে, আর pin কোথাও দেখা যাবে কি?

import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout,
                    format="%(levelname)s:%(name)s:%(message)s")
log = logging.getLogger("wallet.payment")

def pay(balance, amount, pin):
    log.debug("pay() ডাকা হলো: balance=%s amount=%s", balance, amount)
    if amount <= 0:
        log.warning("অবৈধ পরিমাণ বাতিল: %s", amount)
        return balance
    if amount > balance:
        log.error("ব্যালান্স কম: চাওয়া %s, আছে %s", amount, balance)
        return balance
    log.info("পেমেন্ট সফল: %s টাকা, বাকি %s", amount, balance - amount)
    return balance - amount

b = pay(1000, 300, pin="4821")
b = pay(b, -50, pin="4821")
b = pay(b, 5000, pin="4821")

আউটপুট তিন লাইনের: INFO:wallet.payment:পেমেন্ট সফল: 300 টাকা, বাকি 700, তারপর WARNING:wallet.payment:অবৈধ পরিমাণ বাতিল: -50, আর ERROR:wallet.payment:ব্যালান্স কম: চাওয়া 5000, আছে 700. তিনবার log.debug ডাকা হয়েছিল, কিন্তু সীমা INFO, তাই একটিও ছাপা হয়নি — level=logging.DEBUG করে আবার চালালে দেখবেন। pin ফাংশনে এসেছে, কিন্তু কোনো log লাইনে লেখা হয়নি — ইচ্ছা করেই (অংশ ৮)। দুটি জিনিস এই উদাহরণের জন্য বিশেষ: stream=sys.stdout (logging স্বাভাবিকভাবে লেখে stderr-এ), আর format-এ সময় (%(asctime)s) রাখা হয়নি, যাতে প্রতিবার আউটপুট হুবহু একই থাকে। বাস্তব প্রোগ্রামে সময় অবশ্যই রাখবেন।

%(levelname)s, %(name)s, %(message)s হলো format-এর জায়গা-রাখা চিহ্ন। আরও আছে %(asctime)s (সময়), %(filename)s, %(lineno)d (কোন ফাইলের কোন লাইন থেকে), %(funcName)s. আর logger-এর নাম হিসেবে বাস্তবে প্রায় সবসময় লেখা হয় logging.getLogger(__name__) — __name__ হলো module-এর নাম (পর্ব ২২), যেমন wallet.payment. তাতে প্রতিটি log লাইন দেখে বোঝা যায় কোন module থেকে এসেছে, আর পরের অংশে দেখবেন, নামের বিন্দুগুলো একটি বংশতালিকাও বানায়।

৮ · logging ঠিকভাবে — ফাইল, traceback, secret আর একবারের কনফিগারেশন

ফাইলে লেখা আর বাস্তব format

পর্দার লেখা প্রোগ্রাম বন্ধ হলেই হারায়; লগবুক রাখতে হয় ফাইলে। বাস্তব অ্যাপের কনফিগারেশন সাধারণত এমন, প্রোগ্রামের প্রবেশদ্বারে (যেমন main.py):

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),   # ফাইলে
        logging.StreamHandler(),                             # পর্দায় (stderr)
    ],
)

এখানে নতুন শব্দ handler: বার্তা কোথায় যাবে, তা ঠিক করে handler. FileHandler ফাইলে লেখে, StreamHandler পর্দায়। একটি logger-এর কয়েকটি handler থাকতে পারে, আর প্রতিটির নিজস্ব level ও format থাকতে পারে — যেমন ফাইলে সব DEBUG, পর্দায় শুধু WARNING. বাংলা বার্তার জন্য encoding="utf-8" দিতে ভুলবেন না (পর্ব ২৫)। ফাইল যেন অসীম বড় না হয়, তার জন্য logging.handlers module-এ RotatingFileHandler (আকার ধরে নতুন ফাইল) আর TimedRotatingFileHandler (দিন ধরে নতুন ফাইল) আছে।

logger.exception() — traceback-সহ লেখা

পর্ব ২৪-এ শিখেছিলেন, except-এ ভুল ধরলে অন্তত লিখে রাখতে হয়। কিন্তু log.error(e) লিখলে শুধু বার্তাটি থাকে — কোন লাইনে, কোন পথে ঘটেছিল, তা হারিয়ে যায়। সঠিক উপায়:

try:
    gateway.charge(order_id, amount)
except PaymentError:
    log.exception("অর্ডার %s-এর চার্জ ব্যর্থ", order_id)   # ERROR level + পুরো traceback
    raise                                              # সিদ্ধান্ত ওপরের কোডের

log.exception() লেখে ERROR level-এ, আর বার্তার নিচে পুরো traceback জুড়ে দেয়। এটি শুধু except block-এর ভেতরেই ডাকবেন (বাইরে কোনো চলমান exception নেই)। অন্য level-এ traceback চাইলে log.warning("...", exc_info=True). আর খেয়াল করুন: লগ লিখে খালি raise — ধরেছি শুধু লিখে রাখার জন্য, সামলানোর ক্ষমতা আমার নেই।

Lazy formatting — %s কেন, f-string কেন নয়

এতক্ষণ সব log ডাকে লিখেছি log.info("বাকি %s", balance), f"বাকি {balance}" নয়। কারণ lazy formatting: বার্তা আর মানগুলো আলাদা দিলে logging আগে দেখে বার্তাটি আদৌ ছাপা হবে কি না, আর তবেই string জোড়া লাগায়। সীমা INFO থাকলে log.debug("cart=%s", huge_cart) বিনা খরচে বাদ পড়ে; কিন্তু log.debug(f"cart={huge_cart}")-এ বিশাল string তৈরি হয় আগে, ফেলে দেওয়া হয় পরে। লুপের ভেতরে লাখবার চললে এই পার্থক্য চোখে পড়ার মতো। দ্বিতীয় সুবিধা: সব “পেমেন্ট সফল: %s টাকা” লাইনের ছাঁচ একই থাকে, তাই log বিশ্লেষণের টুলগুলো এদের এক দলে গুনতে পারে। (মনে রাখবেন, এখানেও আর্গুমেন্টগুলো নিজে আগেই হিসাব হয়; খুব দামি হিসাব হলে if log.isEnabledFor(logging.DEBUG): দিয়ে ঘিরে দিন।)

secret কখনো লগে নয়

Password, PIN, OTP, access token, API key, পুরো কার্ড নম্বর — কোনোটিই লগে লিখবেন না। লগ ফাইল সাধারণত অনেক কম সুরক্ষিত: অনেকে পড়তে পারেন, ব্যাকআপে বছরের পর বছর থাকে, অন্য কোম্পানির log সার্ভিসে পাঠানো হয়। লগে একটি OTP বা token মানে যে কেউ লগ পড়তে পারে, সে-ই গ্রাহকের অ্যাকাউন্টে ঢুকতে পারে। সবচেয়ে সাধারণ ভুলগুলো অসাবধানতার: log.debug("request=%s", request_data) — আর request_data-র ভেতরে ছিল password. নিয়ম: পুরো request বা object লগ করবেন না, দরকারি ঘরগুলো বেছে লিখুন; মোবাইল নম্বর দরকার হলে ঢেকে দিন (017*****321); আর code review-তে প্রতিটি log লাইন এই চোখে দেখুন। পর্ব ৫০-এ secret সামলানো নিয়ে আরও বিস্তারিত থাকবে।

একবার কনফিগার, প্রবেশদ্বারে — library বনাম application

logging-এর সবচেয়ে গুরুত্বপূর্ণ নকশা-নিয়ম: কোথায় কী লিখবেন (log ডাক) আর লেখাগুলো কোথায়, কীভাবে যাবে (কনফিগারেশন) — দুটি আলাদা জায়গার কাজ। প্রতিটি module শুধু log = logging.getLogger(__name__) নেয় আর বার্তা লেখে। কনফিগারেশন (basicConfig বা logging.config.dictConfig) হয় একবার, প্রোগ্রামের প্রবেশদ্বারে — সাধারণত if __name__ == "__main__":-এর নিচে (পর্ব ২২)।

আপনি যদি এমন একটি library লেখেন যা অন্যরা import করবে, নিয়ম আরও কড়া: library কখনো basicConfig ডাকবে না, নিজে কোনো handler বসাবে না। কারণ library জানে না তাকে কে, কোথায় ব্যবহার করছে — একটি ওয়েব সার্ভার, যেখানে log যায় JSON হয়ে একটি সার্ভিসে? নাকি একটি ছোট স্ক্রিপ্ট? সিদ্ধান্ত application-এর। আরও একটি কারিগরি কারণ: root logger-এ আগে থেকে handler থাকলে basicConfig আর কিছুই করে না (যদি না force=True দেন), তাই library আগে ডাকলে application-এর কনফিগারেশন চুপচাপ উপেক্ষিত হয়। Library শুধু এটুকু করতে পারে:

import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())   # নিজে কিছু ছাপবে না

Logger-এর বংশতালিকা

Logger-এর নামের বিন্দু একটি বংশতালিকা বানায়: app.payment হলো app-এর সন্তান, আর সবার ওপরে root logger. একটি বার্তা যে logger-এ লেখা হয়, সেখান থেকে বাবা-দাদার handler-গুলোর দিকে ওপরে ওঠে — একে বলে propagation. এজন্যই root-এ একটি handler বসালেই সব module-এর বার্তা সেখানে পৌঁছায়। একটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ নিয়মও আছে — stepper-এর তৃতীয় ধাপে দেখুন:

  1. app.paymentlevel: NOTSEThandler নেই
  2. applevel: INFOFileHandler → app.log
  3. rootlevel: WARNINGStreamHandler (level WARNING) → পর্দা
ধাপ ১বার্তা তৈরি
log = logging.getLogger("app.payment")
log.info("পেমেন্ট সফল: 300 টাকা")

app.payment-এর নিজের level NOTSET
→ কার্যকর level ধার করে বাবা app থেকে: INFO
→ INFO ≥ INFO, বার্তা পাস
ধাপ ২app-এর handler
app.payment-এর নিজের handler নেই → ওপরে ওঠা
app-এর FileHandler (level নেই) → লেখা হলো

app.log:
INFO app.payment: পেমেন্ট সফল: 300 টাকা
ধাপ ৩root-এ — কোন level দেখা হয়?
ওপরে ওঠার সময় root LOGGER-এর level (WARNING)
আর দেখা হয় না!
কিন্তু root-এর HANDLER-এর নিজের level WARNING
→ INFO < WARNING → handler ফেলে দিল

পর্দা: (কিছুই নেই)
ধাপ ৪একটি WARNING বার্তা
log.warning("OTP দুবার ভুল")

app.log:  WARNING app.payment: OTP দুবার ভুল
পর্দা:    WARNING app.payment: OTP দুবার ভুল
ধাপ ৫propagate = False
logging.getLogger("app").propagate = False

log.warning("OTP দুবার ভুল")
app.log:  WARNING app.payment: OTP দুবার ভুল
পর্দা:    (কিছুই নেই — app-এর পরে যাত্রা থামল)
  1. বার্তার জন্ম। app.payment logger-এ একটি INFO বার্তা লেখা হলো। এর নিজের level ঠিক করা নেই (NOTSET), তাই কার্যকর level ধার করে নিকটতম পূর্বপুরুষ থেকে — app-এর INFO. বার্তাটি সীমা পার হলো। logger-এর level দেখা হয় শুধু এই শুরুর জায়গায়।
  2. প্রথম থামা — app. app.payment-এর নিজের কোনো handler নেই, তাই বার্তা উঠল বাবার কাছে। app-এর একটি FileHandler আছে, যার আলাদা level নেই — বার্তাটি app.log ফাইলে লেখা হলো।
  3. root-এ — সূক্ষ্ম নিয়ম। বার্তা আরও ওপরে root-এ উঠল। root logger-এর level WARNING, কিন্তু ওপরে ওঠা বার্তার ক্ষেত্রে পূর্বপুরুষ logger-এর level আর পরীক্ষা করা হয় না — বার্তা সরাসরি তাদের handler-এর কাছে যায়। তবে প্রতিটি handler-এর নিজের level আছে কি না, তা দেখা হয়। root-এর পর্দার handler-এর level WARNING, তাই INFO বার্তাটি এখানে বাদ পড়ল। Logger-এর level আর handler-এর level — দুটি আলাদা ছাঁকনি।
  4. WARNING দুই জায়গাতেই। এবার একটি WARNING বার্তা। শুরুতে পাস, app-এর ফাইলে লেখা, root-এর handler-ও পাস — পর্দাতেও দেখা গেল। একটি log ডাক, দুটি গন্তব্য, আর কোন গন্তব্যে কতটা — পুরোটাই কনফিগারেশনে ঠিক হলো, pay()-এর কোড জানেই না।
  5. যাত্রা থামানো। app logger-এ propagate = False করলে বার্তা app-এর handler পর্যন্ত গিয়ে থেমে যায়, root আর পায় না। একটি সাধারণ বাগের উল্টো দিকও এখান থেকে বোঝা যায়: সন্তান আর বাবা দুজনেরই পর্দার handler থাকলে প্রতিটি বার্তা পর্দায় দুবার ছাপা হয় — “আমার log দুবার আসছে কেন?” প্রশ্নের সবচেয়ে সাধারণ উত্তর।

বাস্তব কাজ — খারাপ সারি লগ করে এগিয়ে যাওয়া

ধরুন একটি স্ক্রিপ্ট ভর্তি পরীক্ষার ফলের একটি ফাইল থেকে শিক্ষার্থীদের GPA আমদানি করে। হাজার সারির মধ্যে কয়েকটিতে GPA খালি বা ভুল লেখা। একটি খারাপ সারির জন্য পুরো আমদানি ক্র্যাশ করা ঠিক নয়, আবার চুপচাপ বাদ দেওয়াও ঠিক নয় (পর্ব ২৪-এর “ভুল গিলে ফেলা”)। সমাধান: খারাপ সারি সারি-নম্বরসহ WARNING-এ লগ করুন, ভালোগুলো নিয়ে এগিয়ে যান, শেষে একটি সারসংক্ষেপ:

import logging
import sys

logging.basicConfig(level=logging.DEBUG, stream=sys.stdout,
                    format="%(levelname)s:%(name)s:%(message)s")
log = logging.getLogger("importer")

rows = ["Rahim,3.75", "Karim,", "Sumi,4.00", "Joy,চার", "Mitu,3.50"]
total, ok = 0.0, 0
for n, row in enumerate(rows, start=1):
    name, _, gpa_text = row.partition(",")
    try:
        gpa = float(gpa_text)
    except ValueError as e:
        log.warning("সারি %d বাদ (%s): %s", n, name, e)
        continue
    log.debug("সারি %d ঠিক: %s=%.2f", n, name, gpa)
    total, ok = total + gpa, ok + 1
log.info("%d-এর মধ্যে %d সারি নেওয়া হলো, গড় GPA %.2f", len(rows), ok, total / ok)

আউটপুট ছয় লাইনের: DEBUG:importer:সারি 1 ঠিক: Rahim=3.75, WARNING:importer:সারি 2 বাদ (Karim): could not convert string to float: '', DEBUG:importer:সারি 3 ঠিক: Sumi=4.00, WARNING:importer:সারি 4 বাদ (Joy): could not convert string to float: 'চার', DEBUG:importer:সারি 5 ঠিক: Mitu=3.50, আর শেষে INFO:importer:5-এর মধ্যে 3 সারি নেওয়া হলো, গড় GPA 3.75 (৩.৭৫ + ৪.০০ + ৩.৫০ = ১১.২৫, ভাগ ৩)। সীমা DEBUG, তাই ভালো সারির খবরও দেখা যাচ্ছে; level=logging.WARNING করলে শুধু দুটি খারাপ সারির লাইন থাকবে — কাল সকালে কাকে ফোন করে তথ্য ঠিক করতে হবে, তার তালিকা। ValueError-এর বার্তায় repr রূপে মানটি আছে, তাই খালি string ('') পরিষ্কার দেখা যায়। এখানে log.exception ব্যবহার করা হয়নি, কারণ এই ভুল প্রত্যাশিত আর বার্তাটিই যথেষ্ট; traceback দরকার অপ্রত্যাশিত ভুলে।

Structured logging আর warnings module — সংক্ষেপে

বড় সিস্টেমে প্রতিদিন লাখ লাখ log লাইন হয়, যা মানুষ নয়, মেশিন খোঁজে: “গত এক ঘণ্টায় অর্ডার 1042-এর সব লাইন দেখাও।” তখন সাধারণ বাক্যের বদলে প্রতিটি লাইন লেখা হয় নির্দিষ্ট ঘরসহ, প্রায়ই JSON হিসেবে — {"level": "INFO", "event": "payment_ok", "order_id": 1042, "amount": 300}. একে বলে structured logging. Standard library-তে extra={"order_id": 1042} দিয়ে বাড়তি ঘর যোগ করা যায় আর নিজের Formatter লিখে JSON বানানো যায়; structlog-এর মতো third-party library এই কাজ সহজ করে।

আর একটি আত্মীয় module: warnings. logging হলো চলমান প্রোগ্রামের ঘটনার খাতা — অপারেটরের জন্য। warnings হলো কোড লেখা প্রোগ্রামারের জন্য সতর্কবার্তা: “আপনি যে ফাংশন ব্যবহার করছেন, তা পরের version-এ বাদ যাবে।” Library লেখকরা লেখেন warnings.warn("old_api() বাদ যাবে, new_api() ব্যবহার করুন", DeprecationWarning, stacklevel=2). একই সতর্কবার্তা একই জায়গা থেকে বারবার এলেও স্বাভাবিকভাবে একবারই দেখায়, আর DeprecationWarning সাধারণত লুকানো থাকে, শুধু আপনার নিজের __main__ কোড থেকে ডাকলে বা টেস্ট চালানোর সময় দেখায়। চাইলে logging.captureWarnings(True) দিয়ে সব warning logging-এর খাতায় পাঠানো যায়।

C/C++ জানলে

pdb-এর কমান্ডগুলো ইচ্ছা করেই gdb-র মতো: n, s, c, b, p হুবহু একই, gdb-র bt-এর জায়গায় এখানে w. বড় পার্থক্য বাগের ধরনে: C-তে buffer overflow, dangling pointer বা uninitialized মান segmentation fault দেয় বা চুপচাপ মেমরি নষ্ট করে, আর সেগুলো ধরতে sanitizer ও Valgrind লাগে; Python-এ এমন মেমরি-বাগ প্রায় নেই, প্রায় সব ভুল একটি পরিষ্কার traceback দেয়। C-র standard library-তে কোনো logging ব্যবস্থা নেই (fprintf(stderr, ...) বা syslog ভরসা), Python-এ এটি ব্যাটারির মতো সাথেই আসে। বিস্তারিত: C-তে ডিবাগিং — gdb, sanitizer আর Valgrind.

পরীক্ষা ও ইন্টারভিউতে
  • logging-এর পাঁচটি level ক্রমানুসারে? — DEBUG (10) < INFO (20) < WARNING (30) < ERROR (40) < CRITICAL (50); কনফিগার না করলে সীমা WARNING.
  • print-এর বদলে logging কেন? — level, সময় ও উৎস নিজে থেকে যোগ হয়, গন্তব্য (পর্দা/ফাইল) কনফিগারেশনে বদলানো যায়, আর কোড না বদলেই কম-বেশি দেখানো যায়।
  • logger.exception() আর logger.error()-এর পার্থক্য? — দুটোই ERROR level, কিন্তু exception() পুরো traceback জুড়ে দেয়; একে except-এর ভেতরে ডাকতে হয়।
  • pdb-তে n আর s-এর পার্থক্য? — n ফাংশন ডাকসহ পুরো লাইন চালিয়ে পরের লাইনে যায়; s ফাংশনের ভেতরে ঢোকে।
  • def f(x, items=[])-এ সমস্যা কী? — default list একবারই তৈরি হয় আর সব ডাকে ভাগ হয়; লিখুন items=None.

৯ · AI-এর যুগে — AI-কে দিয়ে ডিবাগ করানো, আর AI-এর ডিবাগ-অভ্যাস যাচাই

AI এখন ডিবাগিংয়ের খুব কাজের সঙ্গী — একটি traceback দেখে কয়েক সেকেন্ডে সম্ভাব্য কারণ বলে দিতে পারে। কিন্তু AI-এর উত্তর ততটাই ভালো, যতটা ভালো আপনার দেওয়া প্রমাণ। “আমার কোড কাজ করছে না” লিখলে সে আন্দাজ করে; পুরো traceback, ছোট একটি পুনরুৎপাদনযোগ্য উদাহরণ (অংশ ৬) আর “কী আশা করেছিলাম, কী পেলাম” দিলে সে যুক্তি দিতে পারে। আর AI-এর নিজের কিছু অভ্যাস আছে, যা চোখে রাখতে হয়:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
প্রতিটি ধাপে print("here"), print("data:", data) বসায়প্রোডাকশনে থেকে যায়, level নেই, বন্ধ করা যায় নাlog = logging.getLogger(__name__) আর log.debug(...)
ক্র্যাশ বন্ধ করতে পুরো ফাংশন try/except Exception: pass-এ মুড়ে দেয়বাগ ঠিক হয় না, শুধু লুকায়; ভুল মান নিয়ে প্রোগ্রাম চলেমূল কারণ ঠিক করা; ধরলে নির্দিষ্ট ধরন আর log.exception
পুরো request বা user object লগ করেpassword, OTP, token লগ ফাইলে চলে যায়দরকারি ঘর বেছে লেখা; secret কখনো নয়
প্রতিটি module-এ logging.basicConfig()প্রথমটি ছাড়া বাকিগুলো কোনো কাজ করে না; library-তে থাকলে অন্যের কনফিগারেশন নষ্টকনফিগারেশন একবার, প্রবেশদ্বারে
log ডাকে f-stringবার্তা বাদ পড়লেও string তৈরি হয়log.debug("x=%s", x)
traceback-এর শেষ লাইন দেখে সেখানেই “ঠিক” করে (যেমন None হলে 0 ধরে নেওয়া)লক্ষণ চাপা পড়ে, কারণ থেকে যায়ভুল মান কোথায় জন্মেছে, তা খুঁজে সেখানে ঠিক করা

ডিবাগিংয়ে AI-এর সাহায্য চাওয়ার সময় নিচের কাঠামোটি কপি করে নিজের তথ্য বসান। খেয়াল করুন, এখানে AI-কে সমাধানের আগে কারণ ব্যাখ্যা করতে বলা হয়েছে:

আমি Python 3.12 ব্যবহার করি। নিচের প্রোগ্রামে একটি বাগ আছে। আগে কোড ঠিক করে দিও না — প্রথমে কারণ খুঁজতে সাহায্য করো। ১. আমি কী আশা করেছিলাম: calc_gpa([85, 72, 40]) একটি সংখ্যা দেবে। ২. কী পেলাম (পুরো traceback): [এখানে পুরো traceback পেস্ট করুন, কিছু বাদ না দিয়ে] ৩. Minimal reproducible example (এটি চালালেই বাগ দেখা যায়): [এখানে ১০–২০ লাইনের ছোট কোড] ৪. আমি যা আগেই পরীক্ষা করেছি: [যেমন: points তালিকায় একটি None আছে] তোমার কাজ: ক. traceback নিচ থেকে পড়ে বলো ক্র্যাশ কোথায়, আর ভুল মানটি সম্ভবত কোথায় জন্মেছে। খ. দুই-তিনটি অনুমান দাও, প্রতিটি যাচাই করার একটি নির্দিষ্ট পরীক্ষা (pdb কমান্ড বা f"{x=}" print) সহ। গ. তারপর সমাধান দাও, আর এই বাগের জন্য একটি pytest টেস্ট লেখো। ঘ. কোনো try/except দিয়ে ভুল চাপা দেবে না। debug-এর জন্য print নয়, logging ব্যবহার করবে, আর কোনো password/OTP/token লগ করবে না।

AI-এর পরামর্শ পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১১৯৪৭ সালে একটি কম্পিউটারের ভেতরে সত্যিকারের একটি পোকা (moth) পাওয়া গিয়েছিল — এখান থেকেই কি “bug” শব্দের জন্ম? গল্পটির সত্যি আর মিথ কোনটুকু?

গল্পটি সত্যি, কিন্তু প্রচলিত রূপটি পুরোপুরি নয়। ১৯৪৭ সালের সেপ্টেম্বরে Harvard বিশ্ববিদ্যালয়ে Harvard Mark II নামের একটি electromechanical কম্পিউটারে কাজ চলছিল — হাজার হাজার relay (বিদ্যুৎ-চালিত সুইচ) দিয়ে তৈরি যন্ত্র। একদিন যন্ত্রটি ঠিকমতো কাজ করছিল না। অপারেটররা খুঁজে দেখলেন, একটি relay-এর ভেতরে একটি মথ (moth) আটকে আছে। তাঁরা পোকাটি বের করে দলের লগবুকে টেপ দিয়ে লাগিয়ে দেন, আর পাশে লেখেন: “First actual case of bug being found.” সেই লগবুকের পাতাটি এখন যুক্তরাষ্ট্রের Smithsonian-এর National Museum of American History-র সংগ্রহে আছে।

এই দলের সদস্য ছিলেন Grace Hopper — পরে COBOL আর প্রথম দিকের compiler-এর কাজের জন্য বিখ্যাত। তিনি গল্পটি বহু বছর ধরে বক্তৃতায় বলেছেন, তাই অনেক জায়গায় লেখা থাকে তিনি নিজে পোকাটি পেয়েছিলেন বা “bug” শব্দটি তিনিই চালু করেছিলেন। দুটো দাবিই সম্ভবত ঠিক নয়। পোকাটি পেয়েছিলেন দলের অপারেটর-প্রকৌশলীরা, আর লগবুকের বাক্যটি নিজেই প্রমাণ করে শব্দটি আগে থেকে চালু ছিল — “first actual case” কথাটির রসিকতা এখানেই: এতদিন যাকে রূপক অর্থে “bug” বলা হতো, এবার সত্যিকারের একটি পোকা পাওয়া গেল!

প্রকৌশলে “bug” শব্দের ব্যবহার অন্তত উনিশ শতক পর্যন্ত পেছনে যায়। Thomas Edison ১৮৭০-এর দশকের চিঠিপত্রে যন্ত্রের ছোটখাটো ত্রুটি আর সমস্যাকে “bugs” বলেছেন, আর তাঁর সময়ের প্রকৌশলীদের মধ্যে শব্দটি পরিচিত ছিল। টেলিগ্রাফ ও টেলিফোনের লাইনে অদ্ভুত শব্দ বা গোলমালের প্রসঙ্গেও এর ব্যবহার পাওয়া যায়। ধারণাটি স্বাভাবিক: একটি অদৃশ্য ছোট “পোকা” যন্ত্রের ভেতরে ঢুকে গোলমাল করছে।

তাহলে Mark II-এর গল্পের গুরুত্ব কোথায়? এটি একটি সুন্দর প্রতীক — কম্পিউটার-যুগে শব্দটির জনপ্রিয়তার মুহূর্ত। আর লগবুকটি নিজেই এই পর্বের দ্বিতীয় বিষয়ের একটি প্রাচীন উদাহরণ: দলটি প্রতিটি ঘটনা সময়সহ লিখে রাখত, তাই প্রায় আশি বছর পরেও আমরা জানি সেদিন ঠিক কী ঘটেছিল। একটি দায়িত্বশীল logging সংস্কৃতির এর চেয়ে ভালো বিজ্ঞাপন কমই আছে।

মূল উপলব্ধি: “bug” শব্দ মথের চেয়ে পুরোনো; মথটি ছিল রূপকের আক্ষরিক প্রমাণ। আর গল্পটি আমাদের কাছে পৌঁছেছে শুধু এই কারণে যে কেউ সেদিন লগবুকে লিখে রেখেছিলেন — ডিবাগিং আর logging-এর সম্পর্ক শুরু থেকেই।

প্র ০২Python-এর logging module কোথা থেকে এল? এর নকশা (logger, handler, formatter, বংশতালিকা) এমন কেন, আর অনেকে কেন একে “জটিল” বলেন?

Python-এর প্রথম এক দশকে কোনো মানক logging ব্যবস্থা ছিল না। প্রত্যেক বড় প্রকল্প নিজের ছোট logging কোড লিখত, ফলে একটি অ্যাপে তিনটি library ব্যবহার করলে তিন রকম log, তিন রকম কনফিগারেশন। এই সমস্যার সমাধানে লেখা হয় PEP 282 — “A Logging System”, যার লেখক Vinay Sajip আর Trent Mick. Vinay Sajip-এর বাস্তবায়নটি Python 2.3-এ (২০০৩ সালে) standard library-তে logging module হিসেবে যোগ হয়, আর তিনি বহু বছর ধরে এর রক্ষণাবেক্ষণ করেছেন।

নকশাটি বানানো হয়েছিল Java জগতের জনপ্রিয় log4j library-র অনুপ্রেরণায় — PEP-টি নিজেই সেই প্রভাবের কথা বলে। তাই এর মূল টুকরোগুলো log4j-এর মতো: Logger (যেখানে কোড বার্তা লেখে, নামের বিন্দু দিয়ে বংশতালিকা), Handler (বার্তা কোথায় যাবে), Formatter (বার্তা দেখতে কেমন হবে), আর Filter (আরও সূক্ষ্ম বাছাই)। প্রতিটি টুকরো আলাদা দায়িত্ব নেয়, তাই যেকোনোটি বদলানো যায় অন্যগুলো না ছুঁয়ে।

বংশতালিকার পেছনের যুক্তি হলো বড় সিস্টেম। ধরুন একটি ওয়েব অ্যাপ বিশটি library ব্যবহার করে, আর প্রত্যেকে getLogger(__name__) দিয়ে লেখে। অ্যাপের মালিক root-এ একটি handler বসিয়ে সব বার্তা এক জায়গায় পান; আবার শুধু urllib3-এর বার্তা কমাতে চাইলে logging.getLogger("urllib3").setLevel(logging.WARNING) — একটি লাইন, কারও কোড না ছুঁয়ে। কোথায় লেখা হবে (library) আর কোথায় যাবে (application) — এই বিভাজনই পুরো নকশার প্রাণ, আর এজন্যই library-র basicConfig না ডাকার নিয়ম।

তবে এই নমনীয়তার দাম আছে। নতুনদের কাছে চারটি ধারণা, propagation, দুই রকম level (logger আর handler) — সব মিলিয়ে বিভ্রান্তিকর। “আমার INFO বার্তা আসে না” আর “আমার বার্তা দুবার আসে” — এই দুটি প্রশ্ন Stack Overflow-এ অসংখ্যবার এসেছে, আর দুটিরই উত্তর এই পর্বের stepper-এ। এই অভিযোগের জবাবে পরে basicConfig-এ নতুন সুবিধা যোগ হয় (যেমন Python 3.8-এ force, 3.9-এ encoding), আর loguru-র মতো third-party library জনপ্রিয় হয়েছে যারা “এক লাইনে logging” প্রতিশ্রুতি দেয়। তবু standard library-র module টিকে আছে, কারণ সব library একটি সাধারণ ব্যবস্থায় লিখতে পারে — এটিই এর সবচেয়ে বড় মূল্য।

মূল উপলব্ধি: logging module-এর “জটিলতা” আসলে বড় সিস্টেমের প্রয়োজন থেকে আসা নমনীয়তা। ছোট স্ক্রিপ্টে basicConfig আর getLogger(__name__)-ই যথেষ্ট; বাকি স্তরগুলো আছে সেই দিনের জন্য, যখন আপনার কোড বিশটি library-র সাথে একটি সার্ভারে চলবে।

প্র ০৩logging কেন log.info("x=%s", x) ধাঁচে বার্তা নেয়, যখন f-string আরও পড়তে সুবিধাজনক? এই “lazy” নকশার পেছনে কী কী কারণ আছে?

প্রথম কারণ গতি, এবং এটি সবচেয়ে পরিচিত। log.debug(f"cart={cart}") লিখলে Python প্রথমে f-string হিসাব করে — cart-এর repr তৈরি, string জোড়া — তারপর ফলটি debug()-কে দেয়। debug() দেখে সীমা INFO, আর পুরো string ফেলে দেয়। কাজটি হলো, কিন্তু অকারণে। অন্যদিকে log.debug("cart=%s", cart)-এ debug() পায় একটি ছাঁচ আর একটি object; সে আগে দেখে বার্তাটি ছাপা হবে কি না, আর বাদ পড়লে string জোড়ার কাজটি কখনো হয়ই না। একটি ছোট list-এ পার্থক্য নগণ্য; কিন্তু একটি বড় dict, প্রতি সেকেন্ডে হাজারবার, এমন কোডে চোখে পড়ার মতো। এই ঝুঁকির কারণে pylint-এর মতো linter log ডাকে f-string দেখলে সতর্ক করে।

তবে একটি সাধারণ ভুল ধারণা আছে: “lazy” মানে আর্গুমেন্টগুলোও হিসাব হয় না — তা নয়। log.debug("total=%s", expensive_sum(data))-এ expensive_sum সবসময় চলে, কারণ Python ফাংশন ডাকার আগে সব আর্গুমেন্ট হিসাব করে। lazy শুধু শেষ string-জোড়ার ধাপটি। সত্যিই দামি হিসাব হলে if log.isEnabledFor(logging.DEBUG): দিয়ে ঘিরে দিতে হয়।

দ্বিতীয় কারণ — অনেকের কাছে আরও গুরুত্বপূর্ণ — log বিশ্লেষণ। ছাঁচটি ("পেমেন্ট সফল: %s টাকা") LogRecord-এর ভেতরে msg হিসেবে আলাদা থাকে, আর মানগুলো args-এ। Sentry-র মতো error-tracking সার্ভিস একই ছাঁচের হাজার লাইনকে একটি “ঘটনা” হিসেবে দল বাঁধতে পারে: “এই ভুলটি আজ ৩,০০০ বার হয়েছে”. f-string দিলে প্রতিটি লাইন আলাদা string — ৩,০০০টি “আলাদা” ভুল।

তৃতীয় কারণ নিরাপত্তা ও দৃঢ়তা। লগ করতে গিয়ে প্রোগ্রাম ক্র্যাশ করা সবচেয়ে বাজে ধরনের বাগ। logging-এর ভেতরে string জোড়ার সময় কোনো ভুল হলে (যেমন কোনো object-এর __str__-এ বাগ), module সেটি ধরে stderr-এ একটি বার্তা দেখায়, কিন্তু আপনার প্রোগ্রাম চালিয়ে যায়। f-string-এর ভুল ঘটে আপনার নিজের লাইনে, logging-এর সুরক্ষার বাইরে। ইতিহাসের দিক থেকে %-ধাঁচটি এসেছে এজন্যও যে ২০০৩ সালে এটিই ছিল Python-এর প্রধান format পদ্ধতি; str.format এসেছে পরে, f-string আরও পরে (Python 3.6). পরে Formatter-এ style="{" যোগ হয়েছে, কিন্তু বার্তার ভেতরের lazy আর্গুমেন্ট এখনো %-ধাঁচের।

মূল উপলব্ধি: %s-ধাঁচ শুধু পুরোনো অভ্যাস নয় — এটি ছাঁচ আর মান আলাদা রাখে, যাতে অপ্রয়োজনীয় কাজ এড়ানো যায়, log মেশিন দিয়ে দল বাঁধা যায়, আর লগ লিখতে গিয়ে প্রোগ্রাম না মরে।

প্র ০৪“Debugging কোড লেখার চেয়ে দ্বিগুণ কঠিন; তাই সবচেয়ে চালাক কোড লিখলে আপনি সংজ্ঞা অনুযায়ীই তা ডিবাগ করার মতো চালাক নন” — Kernighan-এর এই উক্তির মানে কী, আর এটি কোড লেখার ধরন কীভাবে বদলায়?

উক্তিটি সাধারণত Brian Kernighan-এর নামে প্রচলিত — C ভাষার সবচেয়ে বিখ্যাত বই The C Programming Language-এর সহলেখক, আর Unix-এর প্রথম দিকের Bell Labs দলের সদস্য। এটি এসেছে তাঁর আর P. J. Plauger-এর লেখা The Elements of Programming Style বই থেকে (১৯৭৮ সালের দ্বিতীয় সংস্করণ; বইটির প্রথম সংস্করণ ১৯৭৪), যার মূল কথা ছিল: চালাকির চেয়ে স্পষ্টতা বড়। ইংরেজিতে এর প্রচলিত রূপ: “Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?” পরে কেউ কেউ একে “Kernighan's law” বলতে শুরু করেন।

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

Python-এ এর ব্যবহারিক রূপ চেনা। তিন স্তরের nested comprehension, যার ভেতরে walrus আর conditional expression — দেখতে চালাক, কিন্তু একটি ভুল মান খুঁজতে হলে debugger-এ n চাপলে পুরো জিনিসটি একবারে চলে যায়, মাঝখানে থামার জায়গা নেই। একই কাজ তিনটি সাধারণ লাইনে লিখলে প্রতিটি মধ্যবর্তী মানের একটি নাম থাকে — p করে দেখা যায়, breakpoint বসানো যায়, log করা যায়। Python-এর নিজের দর্শনও একই কথা বলে: The Zen of Python-এ (import this) আছে “Readability counts” আর “If the implementation is hard to explain, it's a bad idea.”

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

মূল উপলব্ধি: কোড লিখুন ভবিষ্যতের ক্লান্ত, প্রসঙ্গহীন সেই মানুষটির জন্য, যে রাত দুটোয় এটি ডিবাগ করবে — প্রায়ই সেই মানুষটি আপনি নিজেই। স্পষ্টতা কোনো বিলাসিতা নয়, ডিবাগ করার সামর্থ্য।

প্র ০৫আধুনিক সিস্টেমে শুধু log যথেষ্ট নয় — “observability”-র তিন স্তম্ভ logs, metrics আর traces কী, আর একটি পেমেন্ট সার্ভিসে প্রত্যেকটি কোন প্রশ্নের উত্তর দেয়?

“Observability” শব্দটি এসেছে নিয়ন্ত্রণ-তত্ত্ব (control theory) থেকে — ১৯৬০ সালের দিকে প্রকৌশলী Rudolf Kálmán একটি সিস্টেমকে “observable” বলেছিলেন, যদি তার বাইরের আউটপুট দেখে ভেতরের অবস্থা পুরোপুরি বোঝা যায়। সফটওয়্যারে ধারণাটি একই: চলমান সিস্টেমকে না থামিয়ে, debugger না লাগিয়ে, শুধু তার নিজের পাঠানো তথ্য দেখে কি বোঝা যায় ভেতরে কী ঘটছে? একটি সার্ভার যখন হাজার ব্যবহারকারীকে একসাথে সেবা দিচ্ছে, তখন breakpoint() বসানো অসম্ভব — observability-ই একমাত্র পথ।

প্রচলিতভাবে এর তিনটি “স্তম্ভ” বলা হয়। Logs — নির্দিষ্ট ঘটনার বিবরণ, যা এই পর্বে শিখলেন: “অর্ডার 1042-এর চার্জ ব্যর্থ, gateway timeout”. প্রশ্নের উত্তর দেয়: ঠিক কী ঘটেছিল? Metrics — সময়ের সাথে মাপা সংখ্যা: প্রতি মিনিটে কতটি পেমেন্ট, কত শতাংশ ব্যর্থ, গড়ে কত মিলিসেকেন্ড লাগছে। এগুলো খুব কম জায়গা নেয়, গ্রাফে দেখা যায়, আর alert-এর ভিত্তি: “ব্যর্থতার হার ৫%-এর বেশি হলে ফোন করো”. প্রশ্নের উত্তর দেয়: কিছু কি ভুল হচ্ছে, আর কতটা? Traces — একটি অনুরোধ যখন অনেকগুলো সার্ভিসের ভেতর দিয়ে যায় (অ্যাপ → API → পেমেন্ট সার্ভিস → ব্যাংক → SMS সার্ভিস), তখন পুরো যাত্রার একটি মানচিত্র, প্রতিটি ধাপে কত সময় লাগল সহ। প্রশ্নের উত্তর দেয়: কোথায় ধীর বা কোথায় ভাঙল?

তিনটি একসাথে কাজ করে। রাত তিনটায় metrics-এর alert বলল পেমেন্ট ব্যর্থতা হঠাৎ বেড়েছে। Trace দেখাল প্রতিটি ব্যর্থ অনুরোধ আটকে আছে ব্যাংকের সংযোগের ধাপে। সেই সময়ের সেই সার্ভিসের log দেখাল সঠিক ভুলের বার্তা: certificate-এর মেয়াদ শেষ। এদের জোড়া লাগায় একটি সাধারণ “request id” বা “trace id”, যা প্রতিটি log লাইনেও লেখা থাকে — এখানেই structured logging-এর আসল মূল্য।

Distributed tracing-এর আধুনিক ধারণায় বড় প্রভাব ফেলেছিল Google-এর ২০১০ সালের একটি গবেষণাপত্র, যেখানে তাদের ভেতরের Dapper নামের ব্যবস্থার বর্ণনা ছিল। আজ শিল্পের মানক হয়ে উঠছে OpenTelemetry — OpenTracing আর OpenCensus নামের দুটি প্রকল্প মিলে তৈরি একটি open-source প্রকল্প, যা Cloud Native Computing Foundation (CNCF)-এর অধীনে চলে, আর Python-সহ বহু ভাষায় একই পদ্ধতিতে logs, metrics ও traces সংগ্রহ করতে দেয়। তবে “তিন স্তম্ভ” একটি সরলীকরণ — অনেকে মনে করিয়ে দেন, লক্ষ্য তিনটি আলাদা খাতা রাখা নয়, বরং অচেনা প্রশ্নের উত্তর খুঁজতে পারা।

মূল উপলব্ধি: আজকের log.info(...) লাইনটি একটি বড় ছবির প্রথম টুকরো। metrics বলে “কিছু ভুল হচ্ছে”, traces বলে “কোথায়”, আর logs বলে “ঠিক কী” — আর এই পর্বের অভ্যাসগুলো (level, প্রসঙ্গ, secret না লেখা) সেই পুরো ব্যবস্থার ভিত্তি।

অনুশীলন

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

  1. Traceback থেকে রোগনির্ণয়: একটি প্রোগ্রামের traceback-এর শেষ লাইন TypeError: 'NoneType' object is not subscriptable, আর তার ওপরের লাইন name = student["name"]. student এসেছে find_student(roll) ফাংশন থেকে। ক্র্যাশ কোথায়, বাগ সম্ভবত কোথায়, আর কোন দুটি অনুমান প্রথমে যাচাই করবেন?

    ক্র্যাশ হয়েছে student["name"]-এ, কারণ student হলো None, আর None-এ [] চলে না। কিন্তু বাগ সম্ভবত find_student-এর ভেতরে — লক্ষণের জায়গা আর কারণের জায়গা আলাদা। দুটি অনুমান: (ক) রোলটি সত্যিই তালিকায় নেই, আর ফাংশনটি “পাওয়া যায়নি” অবস্থায় কিছু ফেরত না দিয়ে শেষ হয়ে যায় (তাই None); (খ) ফাংশনের কোনো পথে return ভুলে গেছে, বা print করছে কিন্তু return করছে না। যাচাই: find_student-এর শুরুতে breakpoint(), তারপর p roll ও n চেপে দেখুন কোন পথে শেষ হয়; অথবা রোলটির repr দেখুন — input()-এর string '101' আর তালিকার int 101 না মেলাও খুব সম্ভব। সমাধান হতে পারে না-পাওয়ার ক্ষেত্রে স্পষ্ট raise KeyError(...), বা ডাকার জায়গায় None পরীক্ষা।

  2. কোন লাইন ছাপা হবে: logging.basicConfig(level=logging.WARNING, format="%(levelname)s:%(message)s")-এর পর এই ক্রমে ডাকা হলো: log.info("a"), log.error("b"), log.debug("c"), log.critical("d"), log.warning("e"). আউটপুট কী, কোথায় যাবে, আর সীমা DEBUG করলে কী বদলাবে?

    সীমা WARNING (৩০), তাই শুধু ৩০ বা বেশি: ERROR:b, CRITICAL:d, WARNING:e — এই ক্রমে, কারণ log লাইন ডাকার ক্রমেই ছাপা হয়, গুরুত্বের ক্রমে নয়। stream বা filename দেওয়া হয়নি, তাই যাবে stderr-এ। সীমা DEBUG করলে পাঁচটিই: INFO:a, ERROR:b, DEBUG:c, CRITICAL:d, WARNING:e.

  3. একটি MRE বানান (চিন্তার কাজ): আপনার ৩০০ লাইনের একটি বাজার-হিসাবের স্ক্রিপ্ট একটি CSV ফাইল পড়ে, দাম যোগ করে, আর ফল একটি ওয়েব পাতায় দেখায়। যোগফল ভুল আসছে, আর আপনি সন্দেহ করছেন দামগুলো string হিসেবে যোগ হচ্ছে। Stack Overflow বা AI-কে পাঠানোর জন্য একটি minimal reproducible example-এ কী রাখবেন আর কী বাদ দেবেন?

    বাদ: ওয়েব পাতা, CSV ফাইল খোলার কোড, বাকি সব ফাংশন। রাখুন: CSV-এর তিন-চারটি সারি সরাসরি কোডের ভেতরে একটি list হিসেবে (যেমন ["120", "80", "45"]), যোগ করার ঠিক সেই লাইনগুলো, আর একটি print. তারপর লিখুন: আশা করেছিলাম ২৪৫, পেলাম "1208045" (বা যে ভুল ফল আসছে), আর Python version. প্রায়ই এই ছোট কোড চালাতে গিয়েই দেখবেন দামগুলো int() বা float()-এ না বদলেই += দিয়ে string হিসেবে জোড়া হচ্ছে — অর্থাৎ পোস্ট করার আগেই সমাধান। আর আসল ফাইলে গ্রাহকের নাম বা নম্বর থাকলে নমুনা ডেটায় বানানো নাম দিন।

  4. print থেকে logging-এ (একমাত্র কোড লেখার কাজ): নিচের ফাংশনে debug-এর জন্য তিনটি print আছে: শুরুতে print("login called", phone, password), সফল হলে print("login ok"), ব্যর্থ হলে print("ERROR wrong password"). এগুলো logging-এ বদলান — সঠিক level, lazy formatting, আর নিরাপত্তার কথা মাথায় রেখে।

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

    import logging
    
    log = logging.getLogger(__name__)
    
    def login(phone, password):
        masked = phone[:3] + "*****" + phone[-3:]
        log.debug("login() ডাকা হলো: phone=%s", masked)   # password কখনো নয়
        if check_password(phone, password):
            log.info("লগইন সফল: %s", masked)
            return True
        log.warning("ভুল password: %s", masked)
        return False

    তিনটি সিদ্ধান্ত লক্ষ করুন। প্রথমত, password সরিয়ে দেওয়া হয়েছে — মূল কোডের সবচেয়ে বড় বাগ এটিই, আর মোবাইল নম্বরও ঢেকে দেওয়া হয়েছে। দ্বিতীয়ত, ভুল password ERROR নয়, WARNING — প্রোগ্রাম ঠিকমতোই কাজ করেছে, ব্যবহারকারী ভুল করেছেন; তবে একই নম্বরে বারবার WARNING আসা কাউকে অনুমান করে ঢোকার চেষ্টার লক্ষণ হতে পারে। তৃতীয়ত, ফাংশনের ভেতরে basicConfig নেই — কনফিগারেশন হবে প্রোগ্রামের প্রবেশদ্বারে।

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

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

ক্রমএকটি বাগ খোঁজা ও ঠিক করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔁 বাগটি নিশ্চিতভাবে ঘটানো (পুনরুৎপাদন)
  2. ✂️ ইনপুট আর কোড ছেঁটে সবচেয়ে ছোট উদাহরণ বানানো
  3. 💡 একটি নির্দিষ্ট, যাচাইযোগ্য অনুমান করা
  4. 🔍 print বা debugger দিয়ে অনুমান পরীক্ষা করা
  5. 🛠️ মূল কারণ ঠিক করা
  6. ✅ regression test লিখে আবার যাচাই করা

Python-এ logging.basicConfig() বা অন্য কোনো কনফিগারেশন ছাড়া logging.getLogger("app").info("hi") ডাকলে কী হয়?

  • “INFO:app:hi” পর্দায় ছাপা হয়
  • একটি RuntimeError ওঠে, কারণ logging কনফিগার করা হয়নি
  • কিছুই ছাপা হয় না, কারণ স্বাভাবিক সীমা WARNING
  • বার্তাটি app.log ফাইলে লেখা হয়

price = "120" হলে Python 3.8+ এ print(f"{price=}") কী ছাপে?

  • 120
  • price='120'
  • price=120
  • SyntaxError

pdb-তে থেমে থাকা লাইনে total = compute(items) আছে। compute ফাংশনের ভেতরে ঢুকে লাইন ধরে দেখতে কোন কমান্ড দেবেন?

  • n
  • c
  • w
  • s

def add(item, cart=[]): cart.append(item); return cart — এরপর add("চাল") আর তারপর add("ডাল") ডাকলে দ্বিতীয় ডাক কী ফেরত দেয়?

  • ['চাল', 'ডাল']
  • ['ডাল']
  • TypeError
  • None

Python-এর logging-এ log.debug(f"data={data}")-এর চেয়ে log.debug("data=%s", data) ভালো কেন?

  • f-string দিলে logging error দেয়
  • %s-ধাঁচে আউটপুট বেশি সুন্দর দেখায়
  • বার্তা বাদ পড়লে string জোড়ার কাজটি হয়ই না, আর ছাঁচ এক থাকে
  • %s-ধাঁচে data-র ভেতরের secret লুকিয়ে থাকে

Python-এ একটি except block-এর ভেতরে ভুলের বার্তা আর পুরো traceback একসাথে লগে লিখতে কোনটি সবচেয়ে উপযুক্ত?

  • print(e)
  • logger.exception("বার্তা")
  • logger.error(e)
  • logger.debug(str(e))

শব্দকোষ

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

মনে রাখুন

  1. ডিবাগিং = বৈজ্ঞানিক পদ্ধতি: পুনরুৎপাদন → অনুমান → পরীক্ষা → খোঁজার জায়গা ছোট করা, শেষে regression test. এলোমেলো বদল নয়।
  2. Traceback নিচ থেকে পড়ুন, কিন্তু মনে রাখুন ক্র্যাশের জায়গা আর বাগ জন্মের জায়গা আলাদা হতে পারে। দ্রুত প্রমাণে f"{x=}", repr, type.
  3. breakpoint() দিয়ে pdb: n পরের লাইন, s ভেতরে, c চালিয়ে যাও, p দেখাও, w stack, b লাইন, শর্ত; ক্র্যাশের পরে python -m pdb বা pdb.pm(). VS Code-এ একই কাজ F9, F10, F11, watch আর conditional breakpoint দিয়ে।
  4. চেনা বাগ: off-by-one, mutable default, alias, built-in নাম ঢাকা, ভুল indentation, return ভুলে None, input()-এর string. আটকে গেলে রাবার হাঁস, MRE, আর “কাল চলছিল” হলে git bisect.
  5. logging: getLogger(__name__), পাঁচ level (স্বাভাবিক সীমা WARNING), %s-ধাঁচ, except-এ log.exception, কনফিগারেশন একবার প্রবেশদ্বারে (library কখনো নয়), আর password/OTP/token কখনো লগে নয়।

পরের পর্বে: একসাথে অনেক কাজ — thread, process আর Python-এর বিখ্যাত GIL, সঙ্গে নতুন free-threaded Python. একসাথে চলা কাজের বাগ সবচেয়ে কঠিন, আর সেখানে এই পর্বের logging (কোন thread থেকে কোন বার্তা) হবে আপনার প্রধান চোখ।

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

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