The try and except blocks form the foundation of Python exception handling. Once you understand them, the next important concept is the else block.
The else block is used when you want some code to execute only when the code inside the try block completes successfully without raising an exception.
The general structure is:
try:
# code that may raise an exception
except SomeException:
# code executed if exception occurs
else:
# code executed if no exception occurs
The easiest way to understand this structure is:
try
↓
Did an exception occur?
↙ ↘
Yes No
↓ ↓
except else
↓ ↓
Handle error Success code
The else block is therefore different from the except block. The except block handles failure, while the else block handles the successful path after the risky operation has completed.
Consider a simple number conversion:
try:
number = int("100")
except ValueError:
print("Invalid number")
else:
print("Conversion successful")
print("Number:", number)
Since "100" can successfully be converted into an integer, no exception occurs.
Therefore, the except block is skipped and the else block executes.
The output is:
Conversion successful
Number: 100
Now change the value:
try:
number = int("hello")
except ValueError:
print("Invalid number")
else:
print("Conversion successful")
print("Number:", number)
This time the conversion raises a ValueError.
Python therefore executes the except block and does not execute the else block.
The output is:
Invalid number
You might wonder why we need an else block when we could simply put more statements after the try-except structure.
For example:
try:
number = int("100")
except ValueError:
print("Invalid number")
print("Conversion successful")
There is a problem with this approach. The final statement will execute even if the conversion fails.
For example, if:
number = int("hello")
raises an exception, the message:
Conversion successful
could still be printed if the exception is handled.
Using else makes the intention explicit:
try:
number = int("100")
except ValueError:
print("Invalid number")
else:
print("Conversion successful")
Now the success message executes only when the operation inside try succeeds.
This creates a cleaner separation between:
Let’s look at the complete flow.
try:
risky operation
except:
handle exception
else:
execute success code
If the risky operation succeeds:
try
↓
Success
↓
else
If the risky operation fails:
try
↓
Exception
↓
except
The else block is therefore executed only when the try block completes without an exception.
Consider a program that asks the user to enter their age.
try:
age = int(
input("Enter your age: ")
)
except ValueError:
print(
"Please enter a valid number."
)
else:
print(
"Your age is:",
age
)
If the user enters:
25
the conversion succeeds and the else block executes.
If the user enters:
twenty-five
the ValueError handler executes instead.
This structure clearly separates the two paths.
The else block can be useful when processing analytical data.
Suppose we receive a sales value as text:
sales_value = "50000"
We want to convert it to a number and then perform further calculations only if the conversion succeeds.
try:
sales = float(sales_value)
except ValueError:
print(
"Invalid sales value."
)
else:
average = sales / 10
print(
"Average:",
average
)
The conversion belongs inside try because it may fail.
The analytical calculation can be placed inside else because it should happen only after the conversion has successfully completed.
This makes the code easier to understand.
One try block can be followed by multiple except blocks.
For example:
try:
number = int(
input("Enter a number: ")
)
result = 100 / number
except ValueError:
print(
"Please enter a valid integer."
)
except ZeroDivisionError:
print(
"Zero cannot be used."
)
else:
print(
"Result:",
result
)
There are two possible exceptions here.
If the user enters:
hello
Python raises ValueError.
If the user enters:
0
Python raises ZeroDivisionError.
If the user enters:
20
the calculation succeeds:
100 / 20
and the else block executes.
It is useful to think about exceptions according to the action the program should take.
For example:
| Exception | Possible Response |
|---|---|
ValueError |
Ask for valid input |
ZeroDivisionError |
Ask for a non-zero value |
FileNotFoundError |
Inform the user that the file is missing |
KeyError |
Handle missing data field |
IndexError |
Check the available index range |
Multiple except blocks allow us to provide these different responses in the same program.
The order of except blocks matters because Python checks them from top to bottom.
Consider:
try:
number = int("hello")
except ValueError:
print("Value error")
except Exception:
print("Some other exception")
The ValueError handler is checked first.
Because the actual exception is a ValueError, that handler executes.
The broader Exception handler is not reached.
Generally, when using multiple handlers, more specific exceptions should appear before broader exception classes.
Sometimes two or more exceptions should receive the same response.
Python allows multiple exception types to be grouped in one except statement.
try:
value = int(
input("Enter a number: ")
)
except (ValueError, TypeError):
print(
"Invalid input."
)
Here, both ValueError and TypeError are handled by the same block.
This can be useful when different exceptions require exactly the same response.
We can also capture the exception object:
try:
number = int("hello")
except ValueError as error:
print(
"Conversion failed:",
error
)
The variable error contains information about the exception.
This is useful for logging, debugging, and understanding what happened.
However, when building applications for normal users, technical exception messages should be handled carefully. Users generally need a clear explanation of what they should do next rather than a complicated traceback.
Let’s create a small function for processing a sales value.
def process_sales(value):
try:
sales = float(value)
except ValueError:
print(
"Invalid sales value:",
value
)
else:
tax = sales * 0.18
total = sales + tax
print(
"Sales:",
sales
)
print(
"Tax:",
tax
)
print(
"Total:",
total
)
Now call:
process_sales("10000")
The conversion succeeds, so the else block calculates the tax and total.
Now:
process_sales("unknown")
The conversion raises ValueError, so the except block handles the problem.
This is a realistic example because analytical programs frequently need to validate incoming values before performing calculations.
A good exception-handling design generally keeps the try block focused on operations that may actually raise the exception we are expecting.
For example:
try:
sales = float(value)
except ValueError:
print("Invalid sales")
else:
total = sales * 1.18
print(total)
This is often clearer than putting everything inside try:
try:
sales = float(value)
total = sales * 1.18
print(total)
except ValueError:
print("Invalid sales")
The second version can still be valid, but the first version communicates the intention more clearly: the conversion is the operation that is expected to raise ValueError, while the calculation belongs to the successful path.
An important point is that the else block is not automatically protected from exceptions.
Consider:
try:
number = int("100")
except ValueError:
print("Invalid number")
else:
result = 100 / 0
print(result)
The conversion succeeds, so Python enters the else block.
Then:
100 / 0
raises a ZeroDivisionError.
The except ValueError block does not handle this because it is specifically designed for ValueError raised from the try block.
This demonstrates why we should carefully decide which operations belong inside each section.
The complete structure can be visualized as:
┌──────────────┐
│ try │
└──────┬───────┘
│
operation
│
┌────────┴────────┐
│ │
Exception Success
│ │
↓ ↓
except else
│ │
↓ ↓
Handle error Continue success
This structure is useful because each section has a clear responsibility.
try: Perform an operation that may fail.
except: Respond to an exception.
else: Execute success-only code.
Create a program that asks the user for a number.
Requirements:
try to convert the input to an integer.except ValueError for invalid input.else to print the square of the number.For example, if the user enters:
10
the output should be:
100
If the user enters:
hello
the program should display a meaningful error message.
Create a function that accepts a sales value as input.
Use:
try
except
else
The function should:
else block.Test it with:
"50000"
"12500.50"
"invalid"
else block runs only when the try block completes without an exception.except block handles matching exceptions.except blocks can handle different exception types.else block is useful for code that should execute only after a risky operation succeeds.try block focused can make exception-handling logic easier to understand.try-except-else when converting and processing external data.else block is not automatically handled by an except block designed for the try operation.In the next part, we will learn about the finally block, why it is useful for cleanup operations, and how try, except, else, and finally work together in real Python programs.
The finally block is the third major component of Python’s exception-handling structure. We have already learned that try contains code that may raise an exception, except handles an exception, and else runs when the try block completes successfully. The purpose of finally is different: it contains code that should execute whether an exception occurs or not.
The general structure is:
try:
# code that may cause an exception
except SomeException:
# handle exception
else:
# execute when no exception occurs
finally:
# execute whether exception occurs or not
The most important idea to remember is:
finally is generally used for cleanup or actions that must happen regardless of whether the operation succeeds or fails.
For example, imagine a program opening a file. Whether reading the file succeeds or an exception occurs, we may want to make sure the file is closed.
The flow can be visualized as:
try
↓
Operation executed
↓
┌───────┴────────┐
↓ ↓
Exception Success
↓ ↓
except else
└───────┬────────┘
↓
finally
↓
Continue program
The finally block provides a reliable place for cleanup operations.
Consider:
try:
number = 10 / 2
except ZeroDivisionError:
print("Cannot divide by zero")
finally:
print("Execution completed")
The division succeeds, so the except block is skipped.
The finally block still executes.
The output is:
Execution completed
Now change the operation:
try:
number = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
finally:
print("Execution completed")
This time the exception occurs.
The except block executes:
Cannot divide by zero
and then the finally block executes:
Execution completed
So the output is:
Cannot divide by zero
Execution completed
This demonstrates the key behavior of finally.
Many programming operations require resources to be released after use.
Examples include:
Suppose a program opens a file:
file = open(
"sales.txt",
"r"
)
After working with the file, it should be closed.
If an exception occurs while processing the file, we still want the cleanup operation to happen.
A traditional pattern is:
file = None
try:
file = open(
"sales.txt",
"r"
)
data = file.read()
except FileNotFoundError:
print(
"File not found."
)
finally:
if file is not None:
file.close()
Here, the finally block gives us a place to perform the cleanup.
In modern Python, there are often better resource-management techniques such as the with statement for files, which we will study when we cover file handling. However, understanding finally is still important because the concept applies much more broadly.
The most common structure is:
try:
risky_operation()
except SomeException:
handle_error()
finally:
cleanup()
Each block has a different responsibility.
| Block | Purpose |
|---|---|
try |
Contains code that may raise an exception |
except |
Handles a matching exception |
else |
Runs when no exception occurs |
finally |
Runs regardless of whether an exception occurs |
This separation makes complex programs easier to design.
Python allows all four components to be used together.
try:
number = int(
input("Enter a number: ")
)
except ValueError:
print(
"Invalid number."
)
else:
print(
"Number accepted:",
number
)
finally:
print(
"Program execution finished."
)
Suppose the user enters:
25
The flow is:
try
↓
Conversion succeeds
↓
else
↓
finally
The output includes:
Number accepted: 25
Program execution finished.
If the user enters:
hello
the flow becomes:
try
↓
ValueError
↓
except
↓
finally
The else block does not execute because the operation inside try failed.
The finally block still executes.
A finally block can also be used with try without an except block.
try:
print("Starting operation")
finally:
print("Cleanup operation")
The finally block executes after the try block.
This can be useful when a cleanup operation is required regardless of what happens in the protected code.
Consider:
try:
result = 10 / 0
finally:
print(
"Cleanup executed"
)
There is no except block here to handle the ZeroDivisionError.
The finally block still executes:
Cleanup executed
After that, the unhandled exception can continue to propagate.
This is an important distinction.
finally does not automatically handle an exception.
Its primary purpose is to ensure that its code is executed as part of the cleanup or finalization process.
One interesting behavior of finally is that it can execute even when a function contains a return statement.
Consider:
def example():
try:
return "Success"
finally:
print(
"Finally executed"
)
result = example()
print(result)
The try block prepares to return:
Success
But before the function completely finishes, the finally block executes.
The output is:
Finally executed
Success
This demonstrates that finally is designed to run during the final stage of the operation.
Consider a basic file-processing workflow:
file = None
try:
file = open(
"sales.txt",
"r"
)
data = file.read()
print(
"Data loaded successfully."
)
except FileNotFoundError:
print(
"The sales file could not be found."
)
finally:
if file is not None:
file.close()
print(
"File resource released."
)
There are several possible situations.
If the file exists:
try
↓
Open file
↓
Read file
↓
finally
↓
Close file
If the file does not exist:
try
↓
FileNotFoundError
↓
except
↓
finally
In both cases, the finalization logic is reached.
Data Analytics applications frequently work with databases.
Conceptually, a database workflow may look like:
Connect
↓
Execute Query
↓
Process Results
↓
Close Connection
If the query fails, the connection may still need to be closed.
A simplified structure could be:
connection = None
try:
connection = connect_to_database()
data = connection.execute(
"SELECT * FROM students"
)
except Exception as error:
print(
"Database operation failed:",
error
)
finally:
if connection is not None:
connection.close()
print(
"Database connection closed."
)
This example demonstrates the principle rather than a complete database implementation.
The important idea is that resource cleanup should not depend entirely on successful execution.
Imagine a simple analytical process:
Load Data
↓
Validate Data
↓
Calculate Metrics
↓
Generate Result
↓
Release Resources
Some operations may fail because the data is incomplete or invalid.
A simplified example is:
resource = None
try:
resource = load_data_source()
validate_data(resource)
calculate_metrics(resource)
except ValueError:
print(
"The data contains invalid values."
)
except FileNotFoundError:
print(
"The data source is unavailable."
)
finally:
if resource is not None:
release_resource(resource)
print(
"Resources released."
)
This structure separates normal processing from error handling and cleanup.
A common misunderstanding is that finally somehow makes errors disappear.
It does not.
For example:
try:
number = 10 / 0
finally:
print("Finished")
The finally block executes, but the ZeroDivisionError still exists and can propagate if it is not handled elsewhere.
Therefore:
finally
should be understood as a cleanup or finalization mechanism, not as an exception-suppression mechanism.
Use finally when an operation must be performed regardless of whether the main operation succeeds or fails.
Typical examples include:
For example:
try:
perform_operation()
except SomeException:
handle_problem()
finally:
cleanup()
The code communicates the intention clearly.
Students often confuse else and finally.
Remember:
else
↓
Runs only if try succeeds
finally
↓
Runs whether try succeeds or fails
For example:
try:
number = int("100")
except ValueError:
print("Invalid")
else:
print("Success")
finally:
print("Always runs")
With valid input, both else and finally execute.
With invalid input, except and finally execute, but else does not.
Now let’s combine everything we have learned.
try:
# risky operation
except SpecificException:
# handle expected problem
else:
# execute after successful try
finally:
# cleanup or finalization
This structure gives each section a clear responsibility.
Consider a complete example:
def process_number(value):
try:
number = int(value)
except ValueError:
print(
"Invalid number."
)
else:
result = number * 10
print(
"Result:",
result
)
finally:
print(
"Processing completed."
)
process_number("20")
Output:
Result: 200
Processing completed.
Now:
process_number("hello")
Output:
Invalid number.
Processing completed.
This example demonstrates all three paths clearly.
1. Catch specific exceptions
Prefer:
except ValueError:
when you know the expected problem, instead of unnecessarily catching every exception.
2. Keep try blocks focused
Place only the operations that genuinely require exception handling inside try.
3. Use meaningful error messages
A message should help the user or developer understand the problem and, where appropriate, what action to take.
4. Do not silently ignore exceptions
Avoid:
except:
pass
unless there is a very specific and justified reason.
5. Use finally for cleanup
If something must be cleaned up regardless of success or failure, finally can provide a reliable place for that logic.
6. Do not use exceptions as a replacement for every validation check
Normal conditions can often be checked directly. Exception handling is most useful for exceptional or unexpected situations.
7. Remember that finally does not handle an exception
It executes finalization code. An exception still needs an appropriate handler if you want to prevent it from propagating.
Create a function called:
calculate_average(total, count)
The function should:
try for the calculation.ZeroDivisionError.else to print the successful average.finally to print a message indicating that the calculation process has finished.Test it with:
calculate_average(1000, 10)
calculate_average(1000, 0)
finally is used for code that should execute during finalization regardless of whether an exception occurs.finally does not automatically handle or suppress exceptions.try contains potentially risky operations.except handles matching exceptions.else runs only when the try block completes successfully.finally runs after the exception-handling process.try-except-else-finally.In the final part of this lesson, we will build a complete practical exception-handling project and then cover debugging exercises, common mistakes, interview questions, and a complete revision of try, except, else, and finally.
We have now learned how try, except, else, and finally work individually. In this final part, we will combine these concepts in a practical Python project and then use exercises, debugging questions, and interview questions to revise the complete lesson.
The project we will build is a simple Data Processing and Sales Analysis System. It will demonstrate how exception handling can make a data-processing program more reliable when it receives invalid values.
Suppose our application receives sales values as strings:
sales_data = [
"10000",
"25000",
"invalid",
"15000",
"5000"
]
Real-world data can contain unexpected values. A program should therefore not assume that every value can automatically be converted into a number.
We can create a function:
def process_sale(value):
try:
sale = float(value)
except ValueError:
print(
"Invalid sales value:",
value
)
else:
tax = sale * 0.18
total = sale + tax
print(
"Sale:",
sale
)
print(
"Tax:",
tax
)
print(
"Total:",
total
)
finally:
print(
"Processing completed."
)
Now call:
process_sale("10000")
The value can be converted successfully, so the else block executes.
The calculation becomes:
Tax = 10000 × 0.18
= 1800
Total = 10000 + 1800
= 11800
The output will contain the sale amount, tax, total, and the final processing message.
Now try:
process_sale("invalid")
The conversion:
float("invalid")
raises a ValueError.
The except block handles the problem.
The else block does not execute because the conversion failed.
The finally block still executes.
The flow is:
try
↓
float("invalid")
↓
ValueError
↓
except
↓
finally
This is exactly the behavior we want.
Now we can process every value in the dataset.
sales_data = [
"10000",
"25000",
"invalid",
"15000",
"5000"
]
for value in sales_data:
process_sale(value)
The program processes each record independently.
Valid values are converted and analyzed.
The invalid value is identified and handled without stopping the entire loop.
This is an important concept in Data Analytics.
Suppose we have thousands of records and one record contains invalid data. We may not want one bad record to terminate the entire processing workflow.
Instead, we can identify the invalid record and continue processing the remaining data.
Instead of simply printing results, we can return values from the function.
def process_sale(value):
try:
sale = float(value)
except ValueError:
return None
else:
tax = sale * 0.18
total = sale + tax
return total
finally:
print(
"Record processed:",
value
)
Now we can create a list of valid totals:
totals = []
for value in sales_data:
result = process_sale(value)
if result is not None:
totals.append(result)
At the end, totals contains the successfully processed records.
This demonstrates a practical pattern:
Input Data
↓
Try Conversion
↓
Valid?
↙ ↘
Yes No
↓ ↓
Process Handle
↓ ↓
Store Skip/Report
This type of defensive programming is useful when working with real-world datasets.
Suppose our function receives values that may not always be strings or numbers.
def process_sale(value):
try:
sale = float(value)
tax = sale * 0.18
total = sale + tax
except ValueError:
print(
"Invalid value:",
value
)
except TypeError:
print(
"Unsupported data type:",
type(value).__name__
)
else:
print(
"Total:",
total
)
finally:
print(
"Processing finished."
)
Now the program can distinguish between a value that cannot be converted and an incompatible data type.
Exception handling can also be combined with business rules.
Suppose sales must always be greater than or equal to zero.
def validate_sales(value):
try:
sales = float(value)
if sales < 0:
raise ValueError(
"Sales cannot be negative."
)
except ValueError as error:
print(
"Invalid sales:",
error
)
else:
print(
"Valid sales:",
sales
)
finally:
print(
"Validation completed."
)
Now test:
validate_sales("50000")
This is valid.
Test:
validate_sales("-5000")
The value can technically be converted to a number, but it violates our business rule.
We therefore raise a ValueError ourselves.
This demonstrates that exceptions are not limited to errors automatically generated by Python. Developers can deliberately raise exceptions when application rules are violated.
Let’s create a small sales analyzer.
sales_data = [
"10000",
"25000",
"invalid",
"-5000",
"15000",
"30000"
]
valid_sales = []
invalid_sales = []
for value in sales_data:
try:
sales = float(value)
if sales < 0:
raise ValueError(
"Sales cannot be negative."
)
except ValueError as error:
print(
"Invalid record:",
value
)
invalid_sales.append(value)
else:
valid_sales.append(sales)
print(
"Valid record:",
sales
)
finally:
print(
"Record processing completed."
)
print(
"Valid sales:",
valid_sales
)
print(
"Invalid sales:",
invalid_sales
)
This small project combines several important concepts.
The try block performs conversion and validation.
The except block handles invalid data.
The else block stores valid data.
The finally block performs final processing for every record.
The result is a more robust data-processing workflow.
Data Analysts rarely work with perfectly clean data.
Consider a dataset containing:
10000
25000
30000
unknown
15000
N/A
-500
40000
A naive program may fail as soon as it reaches an invalid value.
A robust program can:
This is one reason exception handling is an important Python skill for Data Analytics.
Mistake 1: Catching everything unnecessarily
try:
risky_operation()
except:
print("Error")
This is often too broad.
If you know the expected exception, prefer a specific handler:
except ValueError:
print(
"Invalid value."
)
Mistake 2: Silently ignoring exceptions
try:
risky_operation()
except:
pass
This can hide important problems and make debugging difficult.
Mistake 3: Putting too much code inside try
A very large try block can make it difficult to determine which operation caused an exception.
Keep the protected code focused whenever practical.
Mistake 4: Using finally as an exception handler
Remember:
except
handles exceptions.
While:
finally
performs finalization or cleanup.
Mistake 5: Using else incorrectly
The else block should contain code that should run only if the try block succeeds.
What exception will occur?
try:
number = int("hello")
except ValueError:
print("Invalid input")
else:
print("Valid input")
finally:
print("Finished")
Answer: ValueError.
The except block executes.
The else block does not execute.
The finally block executes.
Expected output:
Invalid input
Finished
What happens here?
try:
number = int("100")
except ValueError:
print("Invalid")
else:
print("Valid")
finally:
print("Finished")
Answer: No exception occurs.
The else block executes, followed by finally.
Expected output:
Valid
Finished
Identify the exception:
try:
result = 100 / 0
except ValueError:
print("Value problem")
finally:
print("Cleanup")
Answer: ZeroDivisionError.
The ValueError handler does not match the exception.
The finally block executes, but because there is no matching handler in this example, the ZeroDivisionError can still propagate.
This is an important reminder that finally does not automatically handle every exception.
What will happen?
try:
number = int("50")
except ValueError:
print("Invalid")
else:
result = number * 2
print(result)
finally:
print("Done")
Answer:
The conversion succeeds, so the else block runs.
The result is:
100
Done
Create a function called:
process_marks(value)
The function should:
ValueError if marks are below 0 or above 100.except.else to calculate the percentage category.finally to display a completion message.Test it with:
85
105
-10
"abc"
Create a program that attempts to open a file called:
students.csv
Use:
try
except
finally
The program should:
FileNotFoundError.Later, when learning Python File Handling, you will learn the with statement, which is usually preferred for safely managing file resources. The purpose of this exercise is to understand the role of finally.
1. What is the purpose of try in Python?
The try block contains code that may raise an exception.
2. What is the purpose of except?
The except block handles a matching exception raised inside the try block.
3. What is the purpose of else?
The else block executes only when the try block completes without an exception.
4. What is the purpose of finally?
The finally block is used for code that should execute during finalization regardless of whether an exception occurs.
5. Can try be used without except?
Yes. A try block can be combined with finally without an except block.
6. Can there be multiple except blocks?
Yes. Multiple except blocks can handle different exception types.
7. Does finally handle exceptions?
No. finally is primarily used for finalization and cleanup. It does not automatically handle an exception.
8. When does else execute?
It executes when no exception occurs inside the try block.
9. Why is finally useful?
It provides a place for cleanup or finalization code that should execute whether the operation succeeds or fails.
10. Why should we avoid unnecessarily broad exception handling?
Because broad handlers can hide unexpected problems and make debugging more difficult.
| Block | When It Runs | Main Purpose |
|---|---|---|
try |
Always attempted | Run potentially risky code |
except |
When a matching exception occurs | Handle the exception |
else |
When try succeeds | Run success-only code |
finally |
As the finalization stage | Cleanup or final actions |
The complete structure is:
try:
risky_operation()
except SomeException:
handle_error()
else:
success_operation()
finally:
cleanup_operation()
The most important thing to remember is the responsibility of each block.
try: Try the operation.
except: Handle the exception.
else: Run success-only code.
finally: Perform finalization or cleanup.
Once this structure becomes familiar, Python exception handling becomes much easier to understand.
Exception handling is especially valuable in Data Analytics because data comes from external sources and cannot always be assumed to be perfect. Files may be missing, values may be invalid, fields may be unavailable, and calculations may encounter unexpected conditions.
A reliable Python program should therefore be designed to deal with these situations deliberately rather than simply stopping whenever something unexpected occurs.
You should now be able to recognize common exceptions, understand how they affect program flow, use try and except to handle problems, use else for successful execution, and use finally for finalization and cleanup.
The next lesson in this chapter will focus on raising exceptions and creating custom exceptions, which allows developers to enforce their own business rules and communicate application-specific problems clearly.