Skip to content

Files I/O, Exceptions and Modules

File handling, exception handling, and modules are three very important parts of Python.
Almost every real project uses them. File handling lets you save and read data. Exceptions let you deal with errors without crashing your program. Modules let you split your code into smaller, reusable pieces.

Files let you save data so it sticks around even after your program stops running. When your program ends, everything stored in variables disappears - but files stay saved on your computer’s disk. Python can work with many types of files like text files, binary files, CSV files, logs, and more.

The basic steps for working with files are:

  1. Open the file
  2. Do something with it (read or write)
  3. Close the file
ModeMeaning
rRead file (must exist)
wWrite (overwrite file)
aAppend (add at end)
xCreate new file
bBinary mode
tText mode
+Read and write

You can open a file using open() and close it manually when you’re done.

file = open("notes.txt", "r", encoding="utf-8")
content = file.read()
print(content)
file.close()

Explanation:

  • open() opens the file
  • read() reads content
  • close() releases the file

Problem: If something goes wrong before close() runs, the file stays open. This can cause bugs or waste memory.

The with statement handles closing the file for you automatically, even if an error happens in the middle.

with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)

Why use with:

  • Automatically closes the file when you’re done
  • Safer and cleaner to write
  • No risk of accidentally leaving files open
graph TD Start --> Open["Open file"] Open --> Work["Read or write data"] Work --> Exit["Exit block"] Exit --> Close["File closes automatically"]

Reading means pulling data out of a file and into your program.

with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
with open("notes.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.strip())

Reading line by line is better for large files because it doesn’t load everything into memory at once.

Writing replaces everything already in the file. Appending adds new content to the end without touching what’s already there.

# Write
with open("notes.txt", "w", encoding="utf-8") as file:
file.write("First line\n")
# Append
with open("notes.txt", "a", encoding="utf-8") as file:
file.write("Second line\n")

Binary files store raw data as bytes, not as readable text. You use these for things like images, videos, and PDFs.

data = b"Hello"
with open("data.bin", "wb") as file:
file.write(data)
with open("data.bin", "rb") as file:
content = file.read()
print(content)

Sometimes you want to save a whole Python object - like a list or a dictionary - directly into a file and load it back later. This is called serialization.

import pickle
data = {"name": "Sahil", "age": 21}
# Save object
with open("data.pkl", "wb") as file:
pickle.dump(data, file)
# Load object
with open("data.pkl", "rb") as file:
loaded = pickle.load(file)
print(loaded)

Explanation:

  • dump() -> saves the object to the file
  • load() -> reads the object back from the file

pathlib is a modern and cleaner way to work with file paths in Python. It’s easier to use than dealing with raw strings, and it works correctly on all operating systems (Windows, Mac, Linux).

from pathlib import Path
path = Path("data") / "notes.txt"
print(path)

The / operator here is not division - it’s used to join parts of a path together.

Exceptions are errors that happen while your program is running. Instead of just crashing and stopping, Python lets you catch these errors and handle them gracefully.

ExceptionMeaning
FileNotFoundErrorFile not found
ZeroDivisionErrorDivision by zero
ValueErrorInvalid value
TypeErrorWrong type
IndexErrorInvalid index
KeyErrorMissing key
try:
number = int("42")
except ValueError:
print("Invalid number")
else:
print("Success:", number)
finally:
print("Always runs")

Explanation:

  • try -> put the code here that might go wrong
  • except -> this runs if an error happens
  • else -> this runs only if no error happened
  • finally -> this always runs no matter what
graph TD Start --> Try Try -->|No error| Else Try -->|Error| Except Else --> Finally Except --> Finally Finally --> End

You can trigger an error yourself when something in your code doesn’t make sense.

def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age

You can create your own error types to make your program easier to understand and debug.

class InvalidAmountError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InvalidAmountError("Not enough balance")
return balance - amount

Using a custom exception like InvalidAmountError is much clearer than a plain Exception - anyone reading the code immediately knows what went wrong.

A module is just a Python file that holds code you want to reuse. Instead of writing the same functions over and over in every file, you put them in a module and import them wherever you need them.

import math
import math as m
from math import sqrt
from math import sqrt as square_root

helpers.py

def clean_text(text):
return text.strip().lower()

main.py

from helpers import clean_text
print(clean_text(" Hello "))

This trick lets a file work both as a runnable script and as a module that other files can import.

def main():
print("Running directly")
if __name__ == "__main__":
main()

The code inside the if block only runs when you run this file directly. If another file imports it, that block is skipped.

graph TD Script --> Import Import --> Load Load --> Cache["Loaded once"] Cache --> Use

A package is a folder that holds multiple modules. When your project gets big, packages help you keep things organized.

Example structure:

project/
├── main.py
├── utils/
│ ├── __init__.py
│ ├── helpers.py
│ └── parser.py
from utils.helpers import clean_text

Relative imports are used inside packages to import from other files based on where the current file is located - instead of spelling out the full path.

from .helpers import clean_text

This means: import clean_text from helpers.py in the same folder as this file.

from ..config import settings

This means: go one folder up, then import settings from config.py.

project/
├── app/
│ ├── main.py
│ └── utils/
│ └── helper.py

Inside helper.py:

from ..main import something

Explanation:

  • .. -> go up one folder to app/
  • then import something from main.py inside app/
TypeExampleMeaning
Absolutefrom utils.helpers import clean_textFull path from the project root
Relativefrom .helpers import clean_textPath based on where the current file is

Absolute imports are easier to read and understand at a glance. Relative imports are handy when working inside a package and you want to refer to nearby files without writing the full path.