The Transaction That Refused to Disappear
Think about the last time you paid for something using UPI, maybe ₹40 for a plate of chaat, or ₹150 to top up a bus pass. The payment goes through, your phone buzzes, the app shows a green checkmark, and you move on with your day. Open that same app a week later and scroll down: the transaction is still sitting there, exact amount, exact time, exact merchant name. Close the app, restart your phone, switch to a different network. It doesn't budge.
Now compare that to a Python program you've already written yourself: something that asks for five marks, stores them in a list, and prints the average. The moment that script finishes running, every variable inside it is gone. Run it again tomorrow and you start from a blank slate, with no memory of yesterday's marks. That is how every program you've written so far behaves. The question this chapter answers is how to make data outlive the program that created it. The answer is file handling: writing data to a file that lives on your computer's storage, then reading it back whenever you need it, even in a completely different run of the program, on a completely different day.
Why Variables Aren't Enough: RAM Versus Disk
Every variable, list, or dictionary you create in a running Python program is stored in RAM (Random Access Memory), a type of storage built for speed rather than permanence. RAM is volatile: the instant power is cut to it, whether the program ends, the interpreter closes, or the computer shuts down, everything inside it disappears. RAM is fast partly because it doesn't have to worry about long-term durability.
A file lives on your computer's storage drive instead, an SSD or hard disk. This kind of storage is non-volatile: data written to it stays there after the program that wrote it has ended, and even after the computer has been switched off and on again. That is why a UPI app's transaction history survives a phone restart. It isn't sitting in a variable inside a running app; it has been written to storage that doesn't forget. Real payment platforms use secure, structured databases rather than simple text files, but the underlying problem they solve, making data outlast the program, is the same one file handling solves at a smaller scale, and every database still builds on that same idea.
In Python, the tool for reading and writing this kind of persistent data is the built-in open() function.
Opening a File: The open() Function
Before you can read from or write to a file, Python needs to open a connection to it:
file = open("expenses.txt", "w")
This line does two things. It locates (or creates) a file named expenses.txt in the current working directory, and it returns a file object, sometimes called a file handle, which Python uses as a reference point for every read or write operation that follows. You never manipulate the file directly; you always go through this file object.
Notice that "expenses.txt" carries no folder path. This is a relative path, and Python looks for it inside the current working directory: the folder the program is actually running from, which is not always the same as the folder where the .py file happens to be saved. A file that clearly exists in the file explorer can still trigger a file-not-found error simply because the program was launched from a different folder than the one you were looking in. An absolute path, the file's full location on the drive, sidesteps the problem, though for a short program that creates its own file, as in the examples here, a relative path works perfectly well.
The second argument to open() is the mode. It tells Python what you intend to do with the file, and getting it wrong can silently destroy data you meant to keep. The four modes you'll use most often:
"r": Read mode. Opens an existing file for reading only. If the file doesn't exist, Python raises an error. This is the default mode when none is specified."w": Write mode. Creates a new file if none exists. If the file already exists, its entire content is erased the moment it is opened, before you've written a single character."a": Append mode. Creates a new file if none exists, exactly like write mode. But if the file already exists, nothing is erased; new data is simply added after whatever is already there."x": Exclusive creation mode. Creates a new file, but raises an error if the file already exists, protecting you from accidentally overwriting something.
The most common beginner mistake in file handling is opening a file in "w" mode when "a" was intended, watching an entire day's saved data disappear in an instant because "w" truncates the file the moment it opens, with no warning and no undo. A fifth mode, "r+", opens an existing file for both reading and writing at once, but it is used far less often than the four above, since it requires the file to already exist and demands careful tracking of the file pointer to avoid overwriting the wrong bytes.
Writing Data to a File
Once a file is open in write or append mode, you can send text into it using write():
file = open("expenses.txt", "w")
file.write("Bus Pass,150")
file.write("Canteen Lunch,80")
file.close()
Run this, then open expenses.txt in a text editor, and you'll find a small surprise: Bus Pass,150Canteen Lunch,80, mashed onto a single line. Unlike print(), the write() method does not add a newline after each call. If you want each entry on its own line, add "\n" yourself:
file = open("expenses.txt", "w")
file.write("Bus Pass,150\n")
file.write("Canteen Lunch,80\n")
file.close()
When several lines are ready at once, writelines() is more convenient. It accepts a list of strings and writes them one after another, though it still expects you to include the "\n" characters yourself:
transactions = [
"Bus Pass,150\n",
"Canteen Lunch,80\n",
"Mobile Recharge,239\n",
"Stationery,120\n",
"Movie Ticket,200\n"
]
file = open("expenses.txt", "w")
file.writelines(transactions)
file.close()
Closing Files Properly: close() and the with Statement
Every example so far has ended with file.close(), and that isn't a formality. When you write data, Python usually doesn't send it to the disk immediately. For efficiency, it holds recently written data in a temporary memory area called a buffer, then writes it to the disk in batches. Calling close() forces this buffer to empty completely onto the disk, a step called flushing. Skip close(), and some of your data can be left sitting in the buffer, never actually saved, especially if the program crashes before reaching the end. Leaving files open unnecessarily also wastes a limited resource: every operating system caps the number of files a program can have open at once. On Windows in particular, an unclosed file can even stop other programs from opening, renaming, or deleting it until your script finally ends.
The trouble with calling close() manually is that it's easy to forget, and if an error occurs between open() and close(), the closing line may never run at all. Python's solution is the with statement, which wraps file handling in a context manager, a block that guarantees cleanup happens automatically even if the code inside it fails:
with open("expenses.txt", "w") as file:
file.writelines(transactions)
This does exactly what the manual open-write-close version does, except the file closes automatically the instant the indented block ends, whether it ends normally or because of an error. You never had to remember to close the file in the first place. For this reason, with is the standard way to work with files in modern Python, and every example for the rest of this chapter uses it.
Reading Data Back
Python gives you several ways to pull data out of a file, and the right one depends on what you're trying to do.
read() pulls in the entire file as one single string, newlines and all:
with open("expenses.txt", "r") as file:
content = file.read()
print(content)
readline() pulls in just one line per call, moving forward one line each time it's called again. This is useful when you want to process a file a line at a time without loading all of it into memory:
with open("expenses.txt", "r") as file:
first_line = file.readline()
second_line = file.readline()
readlines() reads the whole file and returns it as a list, where each element is one line, still carrying its trailing "\n":
with open("expenses.txt", "r") as file:
lines = file.readlines()
print(lines)
For the file written earlier, this prints:
['Bus Pass,150\n', 'Canteen Lunch,80\n', 'Mobile Recharge,239\n', 'Stationery,120\n', 'Movie Ticket,200\n']
In practice, the more common approach skips both of these and loops directly over the file object itself:
with open("expenses.txt", "r") as file:
for line in file:
print(line.strip())
This gives you one line per iteration, just like readlines(), but without ever holding the entire file in memory at once: Python reads and discards each line as it goes. For a five-line expense file the difference is invisible. For a dataset with two million rows, it's the difference between a program that runs instantly and one that runs out of memory. Notice the .strip() call: it removes the trailing "\n" and any stray whitespace from each line, which you almost always want before working with the text further.
Worked Example: Riya's Expense Tracker
Now combine write mode, append mode, and reading into one complete program. Imagine Riya is starting completely fresh, a brand new folder, no expenses.txt on disk yet, and wants to log every UPI payment she makes today, then get a running total whenever she asks for it.
def log_expense(category, amount):
with open("expenses.txt", "a") as file:
file.write(f"{category},{amount}\n")
def show_total():
try:
total = 0
count = 0
with open("expenses.txt", "r") as file:
for line in file:
category, amount = line.strip().split(",")
total = total + int(amount)
count = count + 1
print(f"{count} transactions found. Total spent: Rs {total}")
except FileNotFoundError:
print("No expenses recorded yet.")
log_expense("Bus Pass", 150)
log_expense("Canteen Lunch", 80)
log_expense("Mobile Recharge", 239)
log_expense("Stationery", 120)
log_expense("Movie Ticket", 200)
log_expense("Auto Fare", 60)
show_total()
log_expense() opens the file in append mode, not write mode, and that choice matters. If it used "w", every call would erase the previous transactions before writing the new one, leaving only the last expense in the file. Because it uses "a", each of the six calls adds exactly one new line at the end, and since expenses.txt doesn't exist before the first call, append mode simply creates it: no error, no missing file crash.
After all six calls, expenses.txt contains:
Bus Pass,150
Canteen Lunch,80
Mobile Recharge,239
Stationery,120
Movie Ticket,200
Auto Fare,60
Now trace what happens inside show_total(). Before the loop starts, total is 0 and count is 0. The for loop then processes one line at a time:
- Line 1,
"Bus Pass,150": split givescategory = "Bus Pass",amount = "150"; converting toint("150")gives 150;total = 0 + 150 = 150,count = 1. - Line 2,
"Canteen Lunch,80":amountbecomes 80;total = 150 + 80 = 230,count = 2. - Line 3,
"Mobile Recharge,239":amountbecomes 239;total = 230 + 239 = 469,count = 3. - Line 4,
"Stationery,120":amountbecomes 120;total = 469 + 120 = 589,count = 4. - Line 5,
"Movie Ticket,200":amountbecomes 200;total = 589 + 200 = 789,count = 5. - Line 6,
"Auto Fare,60":amountbecomes 60;total = 789 + 60 = 849,count = 6.
The loop ends, and the program prints:
6 transactions found. Total spent: Rs 849
Look closely at total = total + int(amount), and specifically at why int(amount) is there at all. Everything that comes out of a text file, no matter how it looks to your eyes, is a string. The line "Mobile Recharge,239" contains the characters '2', '3', and '9', not the number 239. After split(","), amount holds the string "239", and 0 + "239" would crash Python with a TypeError, because an integer and a string cannot be added together. This trips up nearly every beginner at least once: forgetting that reading a file never gives you numbers, only text that happens to look like numbers, which you are always responsible for converting yourself.
One limitation is worth flagging honestly. split(",") only works here because none of Riya's category names contain a comma. A transaction logged as "Gifts, Diwali,500" would break the unpacking, since splitting on every comma would produce three pieces instead of two. Real-world structured data is usually stored as CSV (comma-separated values) with proper rules for quoting a value that contains the delimiter itself, and Python's standard library ships a dedicated csv module that applies those rules correctly instead of a bare split() call. For a personal expense log with short, comma-free category names, the simple approach used here is perfectly fine; for anything larger or messier, that module is the better tool.
Handling a Missing File Gracefully
Look again at the try/except block wrapped around show_total(). If Riya calls show_total() on the very first day, before ever calling log_expense(), expenses.txt won't exist yet, and opening it in "r" mode raises a FileNotFoundError. Without the try/except, this error would crash the entire program with a traceback that means nothing to an end user. Exception handling catches that specific error and responds with something useful instead:
try:
with open("expenses.txt", "r") as file:
data = file.read()
except FileNotFoundError:
print("No expenses recorded yet. Start logging your transactions!")
Any time your program reads a file that might not exist, a file the user is supposed to create, a file left over from a previous run, a file downloaded from somewhere else, wrap the read in a try/except for FileNotFoundError. Assuming a file will always be there is one of the most common reasons a program works perfectly on your laptop and crashes the moment someone else runs it.
There is an alternative style: checking whether the file exists before opening it, using os.path.exists() from Python's standard library.
import os
if os.path.exists("expenses.txt"):
with open("expenses.txt", "r") as file:
data = file.read()
else:
print("No expenses recorded yet.")
Both approaches work, but Python's culture leans toward the try/except version. The guiding idea, sometimes summarised as "easier to ask forgiveness than permission," is that attempting the operation and handling the failure if it happens is usually simpler than checking every precondition in advance, and it sidesteps the small but real risk of the file vanishing in the gap between the check and the actual open. The try/except style also extends naturally to problems beyond a missing file, such as a folder without read permission, simply by catching additional exception types, something a plain existence check cannot do at all.
Finding Your Place: tell() and seek()
Every open file keeps track of exactly where it currently is, using something like a bookmark called the file pointer (or cursor). Each read or write shifts this pointer forward. Two methods let you inspect and control it directly. tell() reports the pointer's current position in the file:
with open("expenses.txt", "r") as file:
first_line = file.readline()
print(file.tell())
seek(0) moves the pointer back to the very beginning of the file. This is useful when you need to read the same file twice within one with block, without closing and reopening it:
with open("expenses.txt", "r") as file:
print(file.read())
file.seek(0)
print(file.read())
Without the seek(0) call, the second file.read() would return an empty string. The pointer would already be sitting at the end of the file from the first read() call, with nothing left to read.
Text Mode and Binary Mode
Every mode covered so far, "r", "w", "a", defaults to text mode, where Python automatically decodes the file's raw bytes into readable string characters using a text encoding such as UTF-8. This works well for .txt, .csv, and .py files, but it breaks down for files that were never meant to be read as text: images, audio, or PDFs. For these, add a "b" to the mode to open the file in binary mode, for example "rb" or "wb", and Python hands you raw bytes instead of decoded text. Opening a photo in text mode typically raises a UnicodeDecodeError, because Python tries to interpret the image's raw bytes as encoded text and fails.
Back to the Transaction Log
Riya's expenses.txt is, in miniature, the same problem a UPI app solves at national scale: data generated by a running program that needs to survive after the program stops. Real payment platforms rely on secure, structured databases built to handle millions of simultaneous transactions safely rather than plain text files, but every database still comes down to the same core idea practiced here: open a connection to persistent storage, write data to it, and read it back reliably, even when something goes wrong along the way.
This same skill is also the entry point into working with real datasets in AI and machine learning. Every dataset a machine learning model trains on, a CSV of house prices, a folder of labelled images, a text corpus for a language model, starts its life inside a program exactly the way expenses.txt did here: opened with open(), read line by line or in bulk, parsed into usable values, and closed safely with a with block. Libraries like pandas will later handle much of this in a single line, such as pd.read_csv(), but underneath that convenience sits the same file opening, line reading, and error handling built by hand in this chapter. Master it here, with six lines in a text file, and it scales up to a dataset with six million rows.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind file handling in python: reading and writing data, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.