অধ্যায় 23

ফাইল পড়া আর লেখা

with open দিয়ে ফাইল পড়া ও লেখা, r/w/a/x মোডের পার্থক্য, প্রতিটি লাইনের শেষের নতুন-লাইন, encoding কেন সবসময় লিখতে হয়, আর pathlib দিয়ে ছোট কাজ।

35 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

তেইশটা অধ্যায় ধরে আমরা যা কিছু বানিয়েছি, সবই প্রোগ্রাম শেষ হওয়ার সাথে সাথে মুছে গেছে।

python
basket = []
basket.append("pen")
print(basket)
text
['pen']

পরের বার চালালে ঝুড়িটা আবার খালি। লিস্ট, ডিকশনারি, ফাংশন — সবকিছু থাকে মেমরিতে, আর মেমরি প্রোগ্রামের আয়ু পর্যন্তই।

গত অধ্যায়ে আমরা শিখেছি কীভাবে কোড একাধিক প্রোগ্রামের মধ্যে ভাগ করা যায়। এই অধ্যায়টা তথ্য নিয়ে — কীভাবে একটা প্রোগ্রামের কাজ পরের বারের জন্য রেখে যাওয়া যায়, বা অন্য কারো জন্য।

python
with open("notes.txt", "w", encoding="utf-8") as fh:
    fh.write("first line\n")
    fh.write("second line\n")

with open("notes.txt", "r", encoding="utf-8") as fh:
    print(fh.read(), end="")
text
first line
second line

প্রোগ্রামটা শেষ হয়ে গেছে, কিন্তু notes.txt রয়ে গেছে।

এই অধ্যায় শেষে আপনি পারবেন

  • with open(...) দিয়ে ফাইল পড়তে ও লিখতে, আর বলতে with কেন
  • "r", "w", "a" আর "x"-এর পার্থক্য বলতে — বিশেষ করে "w" কী মুছে দেয়
  • ফাইল এক লাইন করে পড়তে, আর লাইনের শেষের \n সামলাতে
  • encoding="utf-8" কেন সবসময় লিখতে হয় তা বলতে
  • pathlib.Path দিয়ে ছোট কাজগুলো এক লাইনে সারতে
  • FileNotFoundError আর UnicodeDecodeError পড়ে বুঝতে

পূর্বশর্ত: মডিউল আর ইমপোর্ট।


with কেন

একটা ফাইল খোলা মানে অপারেটিং সিস্টেমের কাছ থেকে কিছু ধার নেওয়া, আর ধার শোধ দিতে হয়। ফাইল বন্ধ না করলে লেখা তথ্য ডিস্কে না-ও পৌঁছাতে পারে, আর অনেকগুলো খোলা ফাইল জমে গেলে প্রোগ্রাম থেমে যায়।

with ব্লকটা সেই শোধটা নিজে করে — ব্লক শেষ হলেই ফাইল বন্ধ, এমনকি ভিতরে কোনো ত্রুটি ঘটলেও।

python
with open("notes.txt", "r", encoding="utf-8") as fh:
    text = fh.read()

print(fh.closed)
print(fh.read())
text
True
ValueError: I/O operation on closed file.

fh নামটা ব্লকের পরেও আছে — স্কোপ ঠিক আগের মতোই কাজ করছে — কিন্তু ফাইলটা বন্ধ। এই কোর্সে open() সবসময় with-এর সাথেই লেখা হবে।

পড়া

ফাইলের ভিতরে কী আছে তা একেবারে পুরোটা পাওয়া যায় .read() দিয়ে:

python
with open("notes.txt", "r", encoding="utf-8") as fh:
    text = fh.read()

print(repr(text))
print(len(text))
text
'first line\nsecond line\n'
23

repr ব্যবহার করা হয়েছে ইচ্ছে করে, কারণ সাধারণ print লাইন-ভাঙাগুলো লুকিয়ে ফেলে। ফাইলটা একটা লম্বা লেখা, আর লাইনের শেষগুলো তার ভিতরে \n হয়ে বসে আছে — এটা মনে রাখা এই অধ্যায়ের অর্ধেক।

এক লাইন করে

বড় ফাইলে .read() ভালো নয়, কারণ সে গোটা ফাইলটাকে মেমরিতে তোলে। ফাইলটার উপর সরাসরি লুপ চালালে পাইথন এক লাইন করে পড়ে:

python
with open("orders.txt", "r", encoding="utf-8") as fh:
    for line in fh:
        print(repr(line))
text
'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'

প্রতিটা লাইনের শেষে \n আছে। এটাই সবচেয়ে সাধারণ বিভ্রান্তি — int(line) কাজ করে না, line == "pen" মেলে না, আর কারণটা চোখে দেখা যায় না।

তাই প্রায় সবসময় প্রথম কাজটা হলো .strip():

python
with open("orders.txt", "r", encoding="utf-8") as fh:
    for line in fh:
        name, price, qty = line.strip().split(",")
        print(f"{name:<5} {float(price) * int(qty):>8.2f}")
text
pen      45.00
bag     850.00
ink     240.00

.strip() তারপর .split(",") তারপর float() আর int() — এই ক্রমটা ফাইল পড়ার কাজে বারবার ফিরে আসবে। ফাইল থেকে যা আসে সবই লেখা, সংখ্যা নয়; সপ্তম অধ্যায়ের সেই রূপান্তরটা এখানেও লাগে।

সবগুলো লাইন একসাথে দরকার হলে দুটো উপায়:

python
with open("orders.txt", "r", encoding="utf-8") as fh:
    lines = fh.read().splitlines()

print(lines)
text
['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']

.splitlines() লাইনের শেষগুলো ফেলে দেয়। .readlines() নামে আরেকটা আছে, কিন্তু সে \n রেখে দেয় — আর তাতেই বেশিরভাগ মানুষ হোঁচট খায়।

লেখা, আর একটা বিপজ্জনক অক্ষর

python
with open("notes.txt", "w", encoding="utf-8") as fh:
    fh.write("new\n")

ফাইলটাতে আগে old content লেখা ছিল। এখন:

text
new

"w" ফাইলটাকে খালি করে দেয় — খোলার মুহূর্তেই, কিছু লেখার আগেই। কোনো সতর্কবার্তা নেই, আর ফেরত যাওয়ার উপায়ও নেই।

আগেরটা রেখে শেষে যোগ করতে চাইলে "a":

python
with open("notes.txt", "a", encoding="utf-8") as fh:
    fh.write("new\n")
text
old content
new

আর ফাইলটা আগে থেকে থাকলে একেবারেই লিখতে না চাইলে "x":

text
FileExistsError: [Errno 17] File exists: 'notes.txt'

চারটে মোড এক জায়গায়: "r" পড়ে, "w" মুছে লেখে, "a" শেষে যোগ করে, "x" কেবল নতুন ফাইল বানায়।

"w" লেখার আগে একবার থামুন। এই এক অক্ষরের ভুলে মানুষ সত্যিকারের কাজ হারায়। যে ফাইলটা খুলছেন সেটা যদি আপনার প্রোগ্রামের তৈরি না হয়, "w" লেখার আগে ভাবুন।

\n আপনাকে দিতে হয়

print নিজে থেকে লাইন ভাঙে। write ভাঙে না।

python
rows = ["pen", "bag"]

with open("out.txt", "w", encoding="utf-8") as fh:
    for row in rows:
        fh.write(row)
text
'penbag'

তাই লাইনের তালিকা লেখার সবচেয়ে পরিষ্কার উপায়:

python
rows = ["pen", "bag"]

with open("out.txt", "w", encoding="utf-8") as fh:
    fh.write("\n".join(rows) + "\n")
text
'pen\nbag\n'

"\n".join(rows) লাইনগুলোর মাঝে ভাঙা বসায়, আর শেষের + "\n" ফাইলটাকে একটা নতুন লাইনে শেষ করে — যেটা টেক্সট ফাইলের প্রথা, আর অনেক টুল সেটাই আশা করে।

encoding="utf-8" — সবসময়

encoding না লিখলে পাইথন অপারেটিং সিস্টেমের ডিফল্ট ব্যবহার করে, যা এক মেশিনে এক রকম আর অন্য মেশিনে অন্য রকম। ফল হলো এমন একটা প্রোগ্রাম যা আপনার এখানে চলে আর অন্য কারো এখানে ভাঙে।

ভুল এনকোডিং-এ পড়লে এটা আসে:

text
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

তাই open() লিখলেই encoding="utf-8" লিখুন — পড়ায় আর লেখায়, দুই জায়গাতেই। ব্যতিক্রম কেবল বাইনারি মোড ("rb", "wb"), যেখানে কোনো এনকোডিং নেই বলেই প্রশ্নটা ওঠে না।

pathlib — ছোট কাজের ছোট পথ

ছোট ফাইলের জন্য গোটা with ব্লকটা বেশি মনে হতে পারে।

python
from pathlib import Path

path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))

out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")
text
True
3
done

read_text আর write_text ভিতরে নিজেরাই ফাইলটা খোলে আর বন্ধ করে, তাই with লাগে না। Path-এর আরও কাজ আছে — .exists(), .name, .suffix, .parent — আর ফোল্ডারের পথ জোড়া লাগে / দিয়ে, যেমন Path("data") / "orders.txt"।

কোনটা কখন? গোটা ফাইলটা একবারে লাগলে Path, আর এক লাইন করে পড়তে হলে with open(...)। write_text-এর আরেকটা সুবিধা আছে: সে ডিফল্ট হিসেবেই ফাইল মুছে নতুন করে লেখে, তাই "w" টাইপ করার ভুলটা অন্তত আকস্মিক হয় না।


একটা সম্পূর্ণ উদাহরণ

orders.txt:

text
pen,15.0,3
bag,850.0,1
ink,120.0,2

main.py:

python
"""Read an order file, total it, and write a report beside it."""

from pathlib import Path

TAX_RATE = 0.15


def read_order(path):
    """Each line is name,price,quantity. Blank lines are skipped."""
    lines = []
    for raw in path.read_text(encoding="utf-8").splitlines():
        if not raw.strip():
            continue
        name, price, quantity = raw.split(",")
        lines.append((name, float(price), int(quantity)))
    return lines


def report(lines):
    rows = []
    total = 0.0
    for name, price, quantity in lines:
        amount = round(price * quantity * (1 + TAX_RATE), 2)
        total += amount
        rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
    rows.append("-" * 20)
    rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
    return rows


def main():
    order = read_order(Path("orders.txt"))
    rows = report(order)

    Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")

    print(Path("report.txt").read_text(encoding="utf-8"), end="")
    print()
    print("lines read:", len(order))


if __name__ == "__main__":
    main()
text
pen      3     51.75
bag      1    977.50
ink      2    276.00
--------------------
total        1305.25

lines read: 3

চারটে জিনিস দেখার মতো।

তিনটে ফাংশনের তিনটে আলাদা কাজ। read_order ডিস্ক থেকে পড়ে, report কেবল হিসাব করে, আর main লেখে। report-কে কোনো ফাইল দেওয়া হয়নি — তাকে দেওয়া হয়েছে একটা লিস্ট, আর সে ফেরত দেয় আরেকটা লিস্ট। তাই তাকে পরীক্ষা করতে কোনো ফাইল লাগে না, আর অষ্টাবিংশ অধ্যায়ে গিয়ে আমরা ঠিক এই কারণেই তাকে ধন্যবাদ দেব।

খালি লাইনটা আগে থেকেই সামলানো। ফাইলের শেষে একটা বাড়তি নতুন লাইন থাকা খুবই স্বাভাবিক, আর সেটা না সামলালে raw.split(",") তিনটে জিনিস না পেয়ে ValueError দিত। if not raw.strip(): continue — এক লাইনে শেষ।

float(price) আর int(quantity) ছাড়া উপায় নেই, কারণ ফাইল থেকে সব আসে লেখা হয়ে। এটা ভুলে গেলে price * quantity চুপচাপ লেখাটাকে পুনরাবৃত্তি করত — আর সেটাই সবচেয়ে খারাপ ধরনের বাগ, যে ভাঙে না।

রিপোর্টটা আলাদা ফাইলে যায়। orders.txt কখনো "w" দিয়ে খোলা হয়নি — ইনপুট ফাইলটা অক্ষত থাকে। এটা অভ্যাস করার মতো: যেটা পড়ছেন সেটাতে লিখবেন না।


কিছু ভাঙা অবস্থা ও তার সমাধান

FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' ফাইলটা নেই, নয়তো আপনি অন্য ফোল্ডার থেকে প্রোগ্রামটা চালাচ্ছেন। পথটা হিসাব হয় যেখান থেকে চালাচ্ছেন সেখান থেকে, স্ক্রিপ্টের ফোল্ডার থেকে নয়। Path("orders.txt").resolve() ছেপে দেখুন সে কোথায় খুঁজছে।

UnicodeDecodeError: 'utf-8' codec can't decode byte ... ফাইলটা অন্য এনকোডিং-এ লেখা, নয়তো সেটা আসলে টেক্সট ফাইলই নয়। encoding ঠিক করুন, অথবা বাইনারি হলে "rb" দিয়ে খুলুন।

ValueError: I/O operation on closed file. with ব্লকের বাইরে গিয়ে ফাইলটা ব্যবহার করা হয়েছে। যা দরকার তা ব্লকের ভিতরেই পড়ে নিন।

ValueError: not enough values to unpack কোনো একটা লাইনে আশানুযায়ী কমা নেই — প্রায়ই সেটা শেষের খালি লাইন। খালি লাইন বাদ দিন, আর সন্দেহ হলে print(repr(raw)) দিয়ে দেখুন।

ফাইলটা লিখলাম, কিন্তু খালি with ব্লক শেষ হওয়ার আগেই ফাইলটা পড়া হচ্ছে। লেখা ডিস্কে পৌঁছায় ফাইল বন্ধ হওয়ার সময়।

সব লাইন একটা লাইনে জোড়া লেগে গেছে write নিজে থেকে লাইন ভাঙে না। "\n" আপনাকে দিতে হবে।

আমার ফাইলের সব তথ্য উধাও "w" দিয়ে খোলা হয়েছিল। পড়ার জন্য "r", শেষে যোগ করতে "a"।