Error Handling: Helping Programs Survive

Welcome back.
In the previous lesson, you learned files.
Your programs learned how to save data and read it later.
You wrote things like:
with open("tasks.txt", "r") as file:
content = file.read()
Very good.
Now your programs can remember things.
Tasks.
Notes.
Contacts.
Small pieces of digital memory.
Beautiful.
But now we have another problem.
Programs crash.
A lot.
Sometimes because the file is missing.
Sometimes because the user enters text instead of a number.
Sometimes because you divide by zero.
Sometimes because the code had one tiny mistake hiding like a ninja behind a bracket.
Example:
age = int(input("Age: "))
If the user types:
banana
Python says:
ValueError
And the program crashes.
Very dramatic.
Very Python.
Very normal.
Error handling helps your program survive problems.
Instead of crashing, your program can respond calmly.
Like:
Please enter a valid number.
Much better.
Much more professional.
Much less “the program exploded because someone typed banana”.
What You Will Learn
In this lesson, you will learn:
- what errors are;
- why programs crash;
- what exceptions are;
- how to use
try; - how to use
except; - how to handle
ValueError; - how to handle
FileNotFoundError; - how to handle
ZeroDivisionError; - how to ask for input again after an error;
- how to avoid catching too much;
- how to combine error handling with files;
- how to build safer beginner programs;
- common beginner mistakes with error handling.
By the end of this lesson, your programs will be more stable.
They will not panic immediately.
They will handle common problems.
They will survive users.
And users are dangerous.
Especially when the user is you at midnight.
What Is an Error?
An error is a problem that stops your program from working correctly.
Example:
number = int("banana")
Python cannot convert "banana" to a number.
So it gives an error:
ValueError
Another example:
result = 10 / 0
Python gives:
ZeroDivisionError
Because dividing by zero is not allowed.
Another example:
with open("missing.txt", "r") as file:
content = file.read()
Python gives:
FileNotFoundError
Because the file does not exist.
Errors are normal.
They are not proof that you are bad at programming.
They are proof that programming is programming.
Welcome.
What Is an Exception?
In Python, many runtime errors are called exceptions.
An exception happens when Python runs into a problem while the program is running.
Examples:
ValueError
FileNotFoundError
ZeroDivisionError
NameError
TypeError
IndexError
KeyError
You have already seen some of these.
Maybe too many.
That is normal.
An exception usually stops the program.
Unless you handle it.
Error handling means:
If something goes wrong, do something safe instead of crashing.
That is what try and except are for.
Very useful.
Very important.
Very “please do not explode”.
Your First try and except
Create a file:
error_handling.py
Write:
try:
number = int(input("Enter a number: "))
print(f"You entered: {number}")
except ValueError:
print("That was not a valid number.")
Run it.
If you enter:
25
Output:
You entered: 25
If you enter:
banana
Output:
That was not a valid number.
The program does not crash.
Excellent.
Python tried to run the risky code.
When it failed, Python jumped to except.
Very civilized.
Very useful.
Much better than screaming in red text.
How try and except Work
Basic structure:
try:
risky_code
except SomeError:
code_to_run_if_error_happens
Example:
try:
age = int(input("Age: "))
print(age)
except ValueError:
print("Please enter a number.")
The try block contains code that might fail.
The except block contains code that runs if the selected error happens.
Think of it like this:
Try this.
If this specific problem happens, do this instead.
It is like a safety net.
Without the safety net, the program falls.
With the safety net, the program says:
Nice try, banana input.
And continues.
Handling ValueError
ValueError often happens when converting input.
Example:
age = int(input("Age: "))
If the user enters:
hello
Python cannot convert it to an integer.
So it raises:
ValueError
Safer version:
try:
age = int(input("Age: "))
print(f"You are {age} years old.")
except ValueError:
print("Age must be a number.")
This is very common.
User input is dangerous.
Users type strange things.
Sometimes by mistake.
Sometimes because they are testing your program.
Sometimes because they are chaos with fingers.
So always be careful with user input.
Asking Again After an Error
Instead of printing an error and stopping, we can ask again.
Example:
while True:
try:
age = int(input("Age: "))
break
except ValueError:
print("Please enter a valid number.")
print(f"You are {age} years old.")
How it works:
Start an infinite loop.
Ask for age.
Try to convert it.
If it works, break the loop.
If it fails, show a message and ask again.
Example:
Age: banana
Please enter a valid number.
Age: hello
Please enter a valid number.
Age: 25
You are 25 years old.
This is much better.
The program does not die.
It educates the user.
Gently.
Like a patient teacher.
But inside, it is probably tired.
Helper Function for Safe Integer Input
We can put this logic into a function.
Create a file:
safe_input.py
Write:
def get_integer(prompt):
while True:
try:
number = int(input(prompt))
return number
except ValueError:
print("Please enter a valid number.")
age = get_integer("Age: ")
print(f"You are {age} years old.")
Now get_integer() keeps asking until the user enters a valid integer.
This is very useful.
You can reuse it many times.
Example:
quantity = get_integer("Quantity: ")
year = get_integer("Year: ")
score = get_integer("Score: ")
Functions plus error handling.
Very good.
Very clean.
Very “we are becoming serious now”.
Handling Float Input
Sometimes you need decimal numbers.
Example:
price = float(input("Price: "))
This can also raise ValueError.
Safer function:
def get_float(prompt):
while True:
try:
number = float(input(prompt))
return number
except ValueError:
print("Please enter a valid number.")
price = get_float("Price: ")
print(f"Price: {price:.2f}")
If the user enters:
abc
The program asks again.
If the user enters:
19.99
It works.
This is useful for:
prices
measurements
averages
discounts
percentages
Basically, anything where decimals enter the room wearing serious shoes.
Handling ZeroDivisionError
Division by zero is not allowed.
Example:
result = 10 / 0
Python gives:
ZeroDivisionError
Safer example:
try:
number = float(input("Number: "))
result = 100 / number
print(f"Result: {result}")
except ZeroDivisionError:
print("You cannot divide by zero.")
except ValueError:
print("Please enter a valid number.")
If the user enters:
0
Output:
You cannot divide by zero.
If the user enters:
banana
Output:
Please enter a valid number.
This handles two possible problems.
Very practical.
Very useful.
Very “the program has seen things”.
Multiple except Blocks
You can handle different errors separately.
Example:
try:
number = int(input("Enter a number: "))
result = 100 / number
print(result)
except ValueError:
print("That was not a valid number.")
except ZeroDivisionError:
print("You cannot divide by zero.")
This is good because different errors need different messages.
ValueError means:
The input was not a number.
ZeroDivisionError means:
The number was zero.
Different problem.
Different response.
Good programs explain problems clearly.
Bad programs say:
Something went wrong.
And then disappear into the fog.
Do not be fog.
Handling FileNotFoundError
You already saw files.
Reading a missing file causes FileNotFoundError.
Example:
try:
with open("notes.txt", "r") as file:
content = file.read()
print(content)
except FileNotFoundError:
print("The file does not exist yet.")
If notes.txt exists, the program reads it.
If it does not exist, the program prints:
The file does not exist yet.
This is much better than crashing.
File errors are common.
Especially when:
the file was never created
the file name is wrong
the file is in another folder
you are running Python from the wrong directory
Classic file drama.
Very normal.
Very fixable.
Loading Tasks Safely
In the previous lesson, we saved tasks in a file.
Now we can load them safely.
Example:
def load_tasks():
tasks = []
try:
with open("tasks.txt", "r") as file:
for line in file:
tasks.append(line.strip())
except FileNotFoundError:
pass
return tasks
If the file exists, tasks are loaded.
If the file does not exist, the function returns an empty list.
This is good.
The first time your program runs, there may be no file yet.
That is not a disaster.
It is just a new program with no data.
Very innocent.
Very empty.
Very ready to become messy later.
Using else with try
Python also supports else with try.
The else block runs only if no error happens.
Example:
try:
number = int(input("Enter a number: "))
except ValueError:
print("Invalid number.")
else:
print(f"Good number: {number}")
If input is valid:
Enter a number: 10
Good number: 10
If input is invalid:
Enter a number: banana
Invalid number.
The else block is optional.
You do not always need it.
But sometimes it makes code clearer.
Simple idea:
try risky thing
except if it fails
else if it succeeds
Very tidy.
Very Python.
Like a small legal contract for errors.
Using finally
Python also has finally.
The finally block runs no matter what.
Example:
try:
number = int(input("Enter a number: "))
print(number)
except ValueError:
print("Invalid number.")
finally:
print("Program finished.")
If the user enters a number, finally runs.
If the user enters bad input, finally still runs.
Example:
Enter a number: banana
Invalid number.
Program finished.
finally is useful when you need to clean up something.
For beginner programs, you will not need it often.
But it is good to know it exists.
Like a fire extinguisher.
You may not use it every day.
But you want it there.
Avoid Catching Everything Too Early
You may see code like this:
try:
number = int(input("Number: "))
except:
print("Something went wrong.")
This catches every error.
It works.
But it is usually not ideal.
Why?
Because it can hide real bugs.
Better:
try:
number = int(input("Number: "))
except ValueError:
print("Please enter a valid number.")
This handles the specific problem you expect.
Specific error handling is better.
It tells you what really happened.
Catching everything is like putting a blanket over the problem.
The problem is still there.
Now it is just warmer.
And harder to see.
Do Not Put Too Much Code in try
Bad:
try:
name = input("Name: ")
age = int(input("Age: "))
city = input("City: ")
print(user_name)
except ValueError:
print("Invalid age.")
This is risky because the try block contains too much.
There may be other errors inside.
For example:
print(user_name)
could cause NameError.
Better:
name = input("Name: ")
try:
age = int(input("Age: "))
except ValueError:
print("Invalid age.")
city = input("City: ")
Keep try focused on the code that may produce the expected error.
This makes debugging easier.
And easier debugging is happiness.
Small happiness.
But still happiness.
Error Handling Is Not for Hiding Bad Code
Important lesson.
Error handling is not a trash bin for broken logic.
Bad idea:
try:
broken_code_here
except:
pass
This hides problems.
The program may continue, but something is wrong.
And now you do not know what.
except: pass can be useful in rare cases.
But beginners should avoid using it casually.
If you ignore every error, your program becomes a mysterious creature.
It runs.
Maybe.
It does things.
Maybe.
Nobody knows why.
This is not engineering.
This is haunted programming.
Avoid.
Mini Program: Safe Age Input
Create a file:
safe_age.py
Write:
while True:
try:
age = int(input("Age: "))
break
except ValueError:
print("Please enter a valid age.")
print(f"Your age is {age}.")
Example:
Age: hello
Please enter a valid age.
Age: 33
Your age is 33.
This program keeps asking until the user enters a valid integer.
Simple.
Useful.
Much better than crashing.
This is a very common beginner pattern.
Use it often.
Your users will type strange things.
Your program should not faint.
Mini Program: Safe Calculator
Create a file:
safe_calculator.py
Write:
try:
first_number = float(input("First number: "))
second_number = float(input("Second number: "))
result = first_number / second_number
print(f"Result: {result:.2f}")
except ValueError:
print("Please enter valid numbers.")
except ZeroDivisionError:
print("You cannot divide by zero.")
Example:
First number: 10
Second number: 2
Result: 5.00
If the user enters:
Second number: 0
Output:
You cannot divide by zero.
This program handles two common problems.
Invalid input.
Division by zero.
Very useful.
Very practical.
Very “the calculator has survival instincts now”.
Mini Program: Safe Notes Reader
Create a file:
safe_notes_reader.py
Write:
try:
with open("notes.txt", "r") as file:
print("Notes:")
for line in file:
print(f"- {line.strip()}")
except FileNotFoundError:
print("No notes found yet.")
If the file exists, it shows notes.
If not, it prints:
No notes found yet.
This is clean.
A missing file is not always a disaster.
Sometimes it just means:
No data yet.
That is a normal situation.
Good programs understand normal situations.
Bad programs panic and throw red text.
We prefer calm programs.
Very Zen.
Very Python.
Mini Program: Safer Task Manager
Now let us improve the task manager.
Create a file:
safe_task_manager.py
Write:
FILE_NAME = "tasks.txt"
def load_tasks():
tasks = []
try:
with open(FILE_NAME, "r") as file:
for line in file:
tasks.append(line.strip())
except FileNotFoundError:
pass
return tasks
def save_tasks(tasks):
with open(FILE_NAME, "w") as file:
for task in tasks:
file.write(task + "\n")
def show_menu():
print("----- Task Manager -----")
print("1. Add task")
print("2. Show tasks")
print("3. Remove task")
print("q. Quit")
def add_task(tasks):
task = input("Task: ")
tasks.append(task)
save_tasks(tasks)
print("Task saved.")
def show_tasks(tasks):
if len(tasks) == 0:
print("No tasks yet.")
else:
print("Tasks:")
for index, task in enumerate(tasks, start=1):
print(f"{index}. {task}")
def remove_task(tasks):
if len(tasks) == 0:
print("No tasks to remove.")
return
show_tasks(tasks)
try:
task_number = int(input("Task number to remove: "))
index = task_number - 1
if index < 0 or index >= len(tasks):
print("Invalid task number.")
return
removed_task = tasks.pop(index)
save_tasks(tasks)
print(f"Removed: {removed_task}")
except ValueError:
print("Please enter a valid number.")
tasks = load_tasks()
while True:
show_menu()
choice = input("Choose an option: ").lower()
if choice == "1":
add_task(tasks)
elif choice == "2":
show_tasks(tasks)
elif choice == "3":
remove_task(tasks)
elif choice == "q":
print("Goodbye.")
break
else:
print("Unknown option.")
This is a stronger program.
It uses:
- files;
- functions;
- lists;
- loops;
try;except;ValueError;FileNotFoundError;enumerate();- safe task removal.
This is real beginner software.
Small, yes.
But real.
It saves data.
Loads data.
Handles missing files.
Handles bad input.
This is how programs grow.
One safety net at a time.
What enumerate() Does
In the task manager, we used:
for index, task in enumerate(tasks, start=1):
print(f"{index}. {task}")
enumerate() gives both:
number
item
Example:
tasks = ["Buy milk", "Study Python"]
for index, task in enumerate(tasks, start=1):
print(f"{index}. {task}")
Output:
1. Buy milk
2. Study Python
This is useful when showing numbered lists.
The user sees task number 1.
Python list index is actually 0.
So when removing a task, we do:
index = task_number - 1
User-friendly number.
Python-friendly index.
Diplomacy.
Between humans and machines.
Very important.
Common Mistake: Handling the Wrong Error
Example:
try:
age = int(input("Age: "))
except FileNotFoundError:
print("File not found.")
This does not make sense.
The risky code is converting input to int.
The likely error is:
ValueError
Correct:
try:
age = int(input("Age: "))
except ValueError:
print("Invalid age.")
Handle the error that can actually happen.
Otherwise your program is guarding the wrong door.
And the bug comes through the window.
Classic bug behavior.
Common Mistake: Too Broad except
Not ideal:
try:
age = int(input("Age: "))
except:
print("Something went wrong.")
Better:
try:
age = int(input("Age: "))
except ValueError:
print("Please enter a valid age.")
Specific is better.
The more specific your error handling is, the easier debugging becomes.
A vague error message helps nobody.
Especially you.
And you are the one who must fix the code.
Be kind to future you.
Future you has already suffered enough.
Common Mistake: Ignoring the Error Completely
Dangerous:
try:
age = int(input("Age: "))
except ValueError:
pass
If the user enters invalid input, nothing happens.
The program silently ignores the problem.
This can create confusing bugs.
Better:
try:
age = int(input("Age: "))
except ValueError:
print("Please enter a valid age.")
At least tell the user what happened.
Silent failure is dangerous.
It is like a smoke alarm that whispers.
Not useful.
Common Mistake: Using a Variable That Was Not Created
Example:
try:
age = int(input("Age: "))
except ValueError:
print("Invalid age.")
print(age)
If the user enters invalid input, age is never created.
Then:
print(age)
can cause:
NameError
Better:
try:
age = int(input("Age: "))
print(age)
except ValueError:
print("Invalid age.")
Or use a loop until valid input is received.
Very important.
If a variable is created inside a try block, make sure it exists before using it later.
Python is not magic.
It will not invent the variable out of pity.
Common Mistake: Error Handling Without Fixing Flow
Bad:
try:
number = int(input("Number: "))
except ValueError:
print("Invalid number.")
result = number * 2
print(result)
If input is invalid, number may not exist.
Better:
while True:
try:
number = int(input("Number: "))
break
except ValueError:
print("Invalid number.")
result = number * 2
print(result)
Now the program only continues when number is valid.
That is good flow.
Error handling is not just about printing messages.
It is about controlling what happens next.
Very important.
Very program architecture.
Small architecture.
But still architecture.
Practice
Create a file:
practice_error_handling.py
Write a program that:
- asks the user for a price;
- asks the user for a quantity;
- calculates the total;
- handles invalid input;
- keeps asking until input is valid.
Example solution:
def get_float(prompt):
while True:
try:
value = float(input(prompt))
return value
except ValueError:
print("Please enter a valid number.")
def get_integer(prompt):
while True:
try:
value = int(input(prompt))
return value
except ValueError:
print("Please enter a valid whole number.")
price = get_float("Price: ")
quantity = get_integer("Quantity: ")
total = price * quantity
print(f"Total: {total:.2f}")
Example:
Price: hello
Please enter a valid number.
Price: 10.50
Quantity: banana
Please enter a valid whole number.
Quantity: 3
Total: 31.50
This is a clean program.
It handles bad input.
It reuses functions.
It calculates safely.
Very good practice.
Very shop-like.
Taxes are still hiding nearby.
But not today.
Mini Challenge
Create a file:
safe_contact_book.py
Build a small contact book that:
- saves contacts to
contacts.txt; - loads contacts from the file;
- lets the user add contacts;
- lets the user show contacts;
- handles missing file safely;
- handles wrong menu options;
- does not crash on first run.
Simple contact format:
name,email,phone
Example structure:
FILE_NAME = "contacts.txt"
def load_contacts():
contacts = []
try:
with open(FILE_NAME, "r") as file:
for line in file:
parts = line.strip().split(",")
if len(parts) == 3:
contact = {
"name": parts[0],
"email": parts[1],
"phone": parts[2]
}
contacts.append(contact)
except FileNotFoundError:
pass
return contacts
def save_contacts(contacts):
with open(FILE_NAME, "w") as file:
for contact in contacts:
file.write(f"{contact['name']},{contact['email']},{contact['phone']}\n")
def add_contact(contacts):
contact = {
"name": input("Name: "),
"email": input("Email: "),
"phone": input("Phone: ")
}
contacts.append(contact)
save_contacts(contacts)
print("Contact saved.")
def show_contacts(contacts):
if len(contacts) == 0:
print("No contacts yet.")
else:
print("Contacts:")
for contact in contacts:
print("-----")
print(f"Name: {contact['name']}")
print(f"Email: {contact['email']}")
print(f"Phone: {contact['phone']}")
def show_menu():
print("----- Contact Book -----")
print("1. Add contact")
print("2. Show contacts")
print("q. Quit")
contacts = load_contacts()
while True:
show_menu()
choice = input("Choose an option: ").lower()
if choice == "1":
add_contact(contacts)
elif choice == "2":
show_contacts(contacts)
elif choice == "q":
print("Goodbye.")
break
else:
print("Unknown option.")
This program combines many things you learned:
- variables;
- strings;
- lists;
- dictionaries;
- loops;
- conditions;
- functions;
- files;
- basic error handling.
That is a lot.
This is not just “Hello, World!” anymore.
This is a small real program.
It saves data.
Loads data.
Uses structured information.
Handles missing files.
Very strong progress.
Very Python.
Beginner Checklist
When your error handling does not work, check:
Did I put risky code inside try?
Am I catching the correct error?
Did I use ValueError for invalid number conversion?
Did I use FileNotFoundError for missing files?
Did I use ZeroDivisionError for division by zero?
Did I accidentally catch everything with bare except?
Am I hiding errors with pass?
Does the program continue safely after an error?
Could a variable be missing after a failed try block?
Should I use a loop to ask again?
Is my try block too large?
Error handling is not about making errors disappear.
It is about responding correctly.
A good program does not pretend problems do not exist.
A good program says:
I expected this problem.
Here is what we do.
Very calm.
Very adult.
Very useful.
Summary
Today you learned:
- errors are normal in programming;
- exceptions happen when Python runs into runtime problems;
trycontains risky code;excepthandles selected errors;ValueErroroften happens with invalid number conversion;FileNotFoundErrorhappens when reading missing files;ZeroDivisionErrorhappens when dividing by zero;- loops can ask for input again after an error;
- helper functions can make safe input reusable;
- multiple
exceptblocks can handle different errors; elseruns when no error happens;finallyruns no matter what;- catching every error can hide bugs;
tryblocks should usually stay focused;- error handling should improve program flow, not hide broken logic.
This is a big step.
Your programs are now safer.
They can handle bad input.
They can handle missing files.
They can avoid dramatic crashes.
They can guide the user instead of exploding.
This is what real programs need.
Because real life is messy.
Files go missing.
Users type bananas.
Numbers become zero.
And sometimes you forget what you wrote yesterday.
Error handling helps your program survive the mess.
Very useful.
Very Python.
Very real.
Next Lesson
In the next lesson, we will learn how to work with modules and imports.
You will learn how to split code into different files and reuse code across your project.
Instead of keeping everything in one giant file, you will organize code like this:
main.py
helpers.py
calculator.py
tasks.py
Then you will import functions from one file into another.
This is another huge step toward real projects.
Because real projects are not one giant file.
Usually.
Unless someone suffered.
And we do not want that.
We want clean structure.
Reusable code.
And fewer files named final_final_really_final.py.
Very Python.
Very next level.