Python Course #9: Error Handling and Exceptions
Error Handling and Exceptions - Part 9 of 10

Welcome to article 9 of 10 of the Python from Scratch Course. So far we have learned to create increasingly complex programs: functions, lists, dictionaries, classes, files... But there is a problem we have not faced directly: what happens when something goes wrong?
Imagine your program asks the user for a number and they type "hello". Or you try to open a file that does not exist. Or you try to divide by zero. In all these cases, Python will throw an error and your program will stop abruptly.
In this article you will learn how to handle errors so your program does not crash, but instead reacts intelligently when something unexpected happens. This is a fundamental skill for any professional programmer.
What Are Errors in Python?
An error is simply something that goes wrong when Python tries to execute your code. There are two main categories of errors:
Syntax errors (SyntaxError)
These errors occur when you write code that Python cannot understand. It is like writing a nonsensical sentence — the interpreter does not know what to do with it.
1# Syntax error: missing closing parenthesis
2print("Hello"
3
4# Syntax error: missing the colon
5if True
6 print("True")
7
8# Syntax error: wrong indentation
9def greet():
10print("Hello") # Missing indentation
Syntax errors are detected before the program runs. Python reads your code, finds something it does not understand, and shows you an error message indicating the problematic line.
Exceptions (runtime errors)
These errors occur while the program is running. The syntax is correct, but something fails during execution.
1# TypeError: trying to add a number with a string
2result = 5 + "hello"
3
4# ValueError: trying to convert text to a number
5number = int("abc")
6
7# ZeroDivisionError: dividing by zero
8result = 10 / 0
9
10# FileNotFoundError: opening a file that does not exist
11file = open("does_not_exist.txt")
12
13# IndexError: accessing an index that does not exist
14my_list = [1, 2, 3]
15print(my_list[10])
16
17# KeyError: looking up a key that does not exist in a dictionary
18data = {"name": "Ana"}
19print(data["age"])
Each of these errors has a specific name (TypeError, ValueError, etc.) that tells you exactly what type of problem occurred.
The try/except Block: Catching Errors
The try/except block is the main tool for handling errors in Python. The idea is simple: "try to execute this code, and if it fails, do this other thing instead of crashing".
Basic syntax
1try:
2 # Code that might fail
3 number = int(input("Enter a number: "))
4 print(f"Your number is: {number}")
5except:
6 # What to do if something fails
7 print("That is not a valid number")
Let us see how it works step by step:
- Python tries to execute the code inside
try - If everything goes well, the
exceptblock is completely ignored - If an error occurs, Python stops executing the
tryand jumps to theexceptblock - After the
except, the program continues normally
Practical example: safe calculator
1# Without error handling - the program crashes
2dividend = int(input("Dividend: ")) # If you type "abc", CRASH
3divisor = int(input("Divisor: ")) # If you type 0, CRASH
4result = dividend / divisor
5print(f"Result: {result}")
6
7# With error handling - the program is robust
8try:
9 dividend = int(input("Dividend: "))
10 divisor = int(input("Divisor: "))
11 result = dividend / divisor
12 print(f"Result: {result}")
13except:
14 print("An error occurred. Please check the data you entered.")
With try/except, if the user types something incorrect, the program shows a friendly message instead of crashing.
Catching Specific Exceptions
Using a generic except (without specifying the error type) works, but it is not recommended. Why? Because you do not know what error occurred, and you cannot give a useful message to the user.
It is much better to catch specific exceptions:
1try:
2 dividend = int(input("Dividend: "))
3 divisor = int(input("Divisor: "))
4 result = dividend / divisor
5 print(f"Result: {result}")
6except ValueError:
7 print("Error: you must enter whole numbers, not letters.")
8except ZeroDivisionError:
9 print("Error: you cannot divide by zero.")
10except Exception as e:
11 print(f"Unexpected error: {e}")
Now the program gives a different message depending on the error type. This is much more useful for the user.
Capturing the error message
You can save the error in a variable using as to access its message:
1try:
2 number = int("hello")
3except ValueError as error:
4 print(f"An error occurred: {error}")
5 # Prints: An error occurred: invalid literal for int() with base 10: 'hello'
Most common exceptions
| Exception | When it occurs | Example |
|---|---|---|
ValueError |
Wrong value for the operation | int("abc") |
TypeError |
Wrong data type | "hello" + 5 |
ZeroDivisionError |
Division by zero | 10 / 0 |
FileNotFoundError |
File not found | open("nofile.txt") |
IndexError |
Index out of range | [1,2][5] |
KeyError |
Key does not exist in dictionary | {"a":1}["b"] |
AttributeError |
Attribute or method does not exist | "hello".append("x") |
ImportError |
Cannot import a module | import noexist |
The else and finally Blocks
In addition to try and except, Python has two additional blocks that make error handling more complete: else and finally.
The else block
The else block runs only if there was NO error in the try. It is useful for separating the code that might fail from the code that depends on everything going well.
1try:
2 number = int(input("Enter a number: "))
3except ValueError:
4 print("That is not a valid number.")
5else:
6 # Only runs if there was no error
7 print(f"Double your number is: {number * 2}")
8 print(f"Half your number is: {number / 2}")
The finally block
The finally block runs always, whether or not there was an error. It is perfect for cleanup tasks, like closing files or connections.
1try:
2 file = open("data.txt", "r")
3 content = file.read()
4 print(content)
5except FileNotFoundError:
6 print("The file does not exist.")
7finally:
8 # This ALWAYS runs
9 print("Read operation finished.")
10
11# Example with a file that always gets closed
12file = None
13try:
14 file = open("data.txt", "r")
15 content = file.read()
16except FileNotFoundError:
17 print("File not found.")
18finally:
19 if file:
20 file.close()
21 print("File closed successfully.")
Complete structure: try/except/else/finally
1try:
2 # Code that might fail
3 number = int(input("Enter a number: "))
4 result = 100 / number
5except ValueError:
6 print("Error: enter a valid number.")
7except ZeroDivisionError:
8 print("Error: you cannot divide by zero.")
9else:
10 # Only if there was no error
11 print(f"100 / {number} = {result}")
12finally:
13 # Always runs
14 print("Thank you for using the calculator.")
try → except → else → finally. You cannot change the order. The else and finally are optional.
Raising Exceptions with raise
So far we have caught errors that Python generates. But you can also generate your own errors on purpose using raise. This is useful when you want to validate data and stop execution if something is not correct.
1def set_age(age):
2 if age < 0:
3 raise ValueError("Age cannot be negative")
4 if age > 150:
5 raise ValueError("Age cannot be greater than 150")
6 return age
7
8# Usage
9try:
10 my_age = set_age(-5)
11except ValueError as e:
12 print(f"Error: {e}")
13 # Prints: Error: Age cannot be negative
When to use raise?
Use raise when your function receives data that does not make sense for your business logic:
1def transfer_money(source, destination, amount):
2 if amount <= 0:
3 raise ValueError("Amount must be positive")
4 if amount > source.balance:
5 raise ValueError("Insufficient balance")
6
7 source.balance -= amount
8 destination.balance += amount
9 return True
10
11def register_user(name, email):
12 if not name or not name.strip():
13 raise ValueError("Name cannot be empty")
14 if "@" not in email:
15 raise ValueError("Email is not valid")
16
17 # Continue with registration...
18 print(f"User {name} registered with {email}")
Creating Custom Exceptions
You can create your own exception types by creating a class that inherits from Exception. This is useful in large projects to have errors specific to your application.
1# Define custom exceptions
2class InsufficientBalanceError(Exception):
3 """Raised when there is not enough balance for the operation."""
4 def __init__(self, current_balance, requested_amount):
5 self.current_balance = current_balance
6 self.requested_amount = requested_amount
7 message = f"Insufficient balance. You have {current_balance}, but need {requested_amount}"
8 super().__init__(message)
9
10class InvalidAgeError(Exception):
11 """Raised when the age is not in a valid range."""
12 pass
13
14class InvalidEmailError(Exception):
15 """Raised when the email format is incorrect."""
16 pass
Using custom exceptions
1class BankAccount:
2 def __init__(self, owner, balance=0):
3 self.owner = owner
4 self.balance = balance
5
6 def withdraw(self, amount):
7 if amount <= 0:
8 raise ValueError("Amount must be positive")
9 if amount > self.balance:
10 raise InsufficientBalanceError(self.balance, amount)
11
12 self.balance -= amount
13 return self.balance
14
15# Usage
16account = BankAccount("Carlos", 1000)
17
18try:
19 account.withdraw(1500)
20except InsufficientBalanceError as e:
21 print(f"Could not withdraw: {e}")
22 print(f"Your current balance: {e.current_balance}")
23 # Prints: Could not withdraw: Insufficient balance. You have $1000, but need $1500
24 # Your current balance: $1000
Error. Examples: InsufficientBalanceError, UserNotFoundError, ConnectionFailedError.
Best Practices for Error Handling
Now that you know how to use try/except, raise, and custom exceptions, let us look at the rules professional programmers follow:
1. Never use a bare except without a type
1# BAD - catches EVERYTHING, even serious errors
2try:
3 result = do_something()
4except:
5 pass # Silences all errors
6
7# GOOD - catches only what you expect
8try:
9 result = do_something()
10except ValueError as e:
11 print(f"Invalid value: {e}")
12except FileNotFoundError as e:
13 print(f"File not found: {e}")
2. Never use pass in an except (silencing errors)
1# BAD - the error disappears and you will never know what failed
2try:
3 data = int(input("Number: "))
4except ValueError:
5 pass
6
7# GOOD - at least log the error
8try:
9 data = int(input("Number: "))
10except ValueError:
11 print("Please enter a valid number.")
12 data = 0 # Default value
3. Put only what is necessary inside try
1# BAD - too much code in try
2try:
3 name = input("Name: ")
4 age = int(input("Age: "))
5 email = input("Email: ")
6 city = input("City: ")
7 print(f"Data: {name}, {age}, {email}, {city}")
8except ValueError:
9 print("Error in age")
10
11# GOOD - only the line that can fail
12name = input("Name: ")
13try:
14 age = int(input("Age: "))
15except ValueError:
16 print("Error: age must be a number")
17 age = 0
18email = input("Email: ")
19city = input("City: ")
4. Use logging instead of print for errors in production
1import logging
2
3logging.basicConfig(level=logging.INFO)
4logger = logging.getLogger(__name__)
5
6try:
7 result = process_data(data)
8except ValueError as e:
9 logger.error(f"Error processing data: {e}")
10except Exception as e:
11 logger.critical(f"Unexpected error: {e}", exc_info=True)
5. Use with for automatic resource management
1# Instead of try/finally to close files, use with
2with open("data.txt", "r") as file:
3 content = file.read()
4# The file is closed automatically, even if there is an error
Practical Example: User Data Validator
Let us build a complete program that asks the user for data and handles all possible errors gracefully:
1class ValidationError(Exception):
2 """Custom error for validations."""
3 pass
4
5def ask_name():
6 """Ask for and validate the user's name."""
7 name = input("Enter your name: ").strip()
8 if not name:
9 raise ValidationError("Name cannot be empty")
10 if len(name) < 2:
11 raise ValidationError("Name must be at least 2 characters")
12 if any(char.isdigit() for char in name):
13 raise ValidationError("Name cannot contain numbers")
14 return name
15
16def ask_age():
17 """Ask for and validate the user's age."""
18 entry = input("Enter your age: ").strip()
19 try:
20 age = int(entry)
21 except ValueError:
22 raise ValidationError(f"'{entry}' is not a valid number")
23
24 if age < 0:
25 raise ValidationError("Age cannot be negative")
26 if age > 120:
27 raise ValidationError("Age does not seem valid (greater than 120)")
28 return age
29
30def ask_email():
31 """Ask for and validate the user's email."""
32 email = input("Enter your email: ").strip()
33 if not email:
34 raise ValidationError("Email cannot be empty")
35 if "@" not in email:
36 raise ValidationError("Email must contain @")
37 if "." not in email.split("@")[1]:
38 raise ValidationError("Email domain must contain a dot")
39 return email
40
41def ask_phone_number():
42 """Ask for and validate a phone number."""
43 phone = input("Enter your phone (10 digits): ").strip()
44 if not phone.isdigit():
45 raise ValidationError("Phone must only contain numbers")
46 if len(phone) != 10:
47 raise ValidationError(f"Phone must have 10 digits, has {len(phone)}")
48 return phone
49
50def ask_with_retries(ask_function, max_attempts=3):
51 """Execute a request function with retries."""
52 for attempt in range(1, max_attempts + 1):
53 try:
54 return ask_function()
55 except ValidationError as e:
56 print(f" Error: {e}")
57 if attempt < max_attempts:
58 print(f" Attempt {attempt} of {max_attempts}. Try again.")
59 else:
60 print(f" You used all {max_attempts} attempts.")
61 return None
62
63def main():
64 print("=" * 50)
65 print(" REGISTRATION FORM")
66 print("=" * 50)
67 print()
68
69 name = ask_with_retries(ask_name)
70 if not name:
71 print("Could not get name. Exiting...")
72 return
73
74 age = ask_with_retries(ask_age)
75 if age is None:
76 print("Could not get age. Exiting...")
77 return
78
79 email = ask_with_retries(ask_email)
80 if not email:
81 print("Could not get email. Exiting...")
82 return
83
84 phone = ask_with_retries(ask_phone_number)
85 if not phone:
86 print("Could not get phone number. Exiting...")
87 return
88
89 # All data is valid
90 print()
91 print("=" * 50)
92 print(" REGISTRATION SUCCESSFUL")
93 print("=" * 50)
94 print(f" Name: {name}")
95 print(f" Age: {age} years")
96 print(f" Email: {email}")
97 print(f" Phone: {phone}")
98
99if __name__ == "__main__":
100 main()
Summary and Next Article
In this article we learned everything about error handling in Python:
- The difference between syntax errors and exceptions
- How to use try/except to catch errors without crashing the program
- The importance of catching specific exceptions (ValueError, TypeError, etc.)
- The else block (runs if no error) and finally block (always runs)
- How to raise exceptions with
raiseto validate data - How to create custom exceptions for your application
- Best practices: avoid bare except, do not silence errors, use logging
- A complete example of a robust program that validates user data
In the next and final article (Part 10 of 10) we are going to build a complete final project: a task manager in the terminal. We will apply absolutely everything we have learned in the course: variables, functions, lists, dictionaries, OOP, files, and error handling.
Get ready to put everything you have learned into practice!
Comments
Sign in to leave a comment
No comments yet. Be the first!