Previous | Tutorial index | Next

📚 Tutorial 1: Python Errors and Exceptions – The Fundamentals

Learning Objectives

Explain different types of errors and exceptions that may occur in a Python program.

1. Introduction: The Reality of Errors in Programming

No matter how experienced a programmer you are, errors are an inevitable part of software development. The key difference between a novice and an expert is not the absence of errors, but the ability to anticipate, understand, and gracefully handle them.

In this tutorial, we will build a solid foundation for understanding what can go wrong in a Python program. We will classify different types of errors, explain the concept of exceptions, explore Python's exception hierarchy, and discuss why proper exception handling is a hallmark of professional, robust software.

2. Key Terms

Before diving into the content, familiarise yourself with these key terms:

Term Definition
Syntax Error An error that occurs when the Python interpreter cannot parse your code because it violates the language's grammar rules.
Logical Error An error where the code runs but produces incorrect results due to a flaw in the programmer's logic.
Runtime Error (Exception) An error that occurs during program execution when an unexpected condition is encountered.
Exception An event that disrupts the normal flow of a program's instructions; represented as an object in Python.
Traceback A detailed report printed when an unhandled exception occurs, showing the call stack and error details.
Exception Hierarchy The class structure where all exceptions inherit from BaseException; most inherit from Exception.
Call Stack The sequence of function calls that led to an error, shown in the traceback.

3. Real‑World Analogy: A Day at the Coffee Shop

Imagine you are a barista following a recipe to make a latte. The recipe is like your Python code.

4. What is an Error in Python?

We divide errors into three main categories:

4.1 Syntax Errors (Parsing Errors)

These occur when the interpreter cannot parse your code because it violates Python's grammar.

Key Characteristics:

Example:

print("Hello, World!" # Missing closing parenthesis → SyntaxError

Another example:

if x > 5 print("x is greater than 5") # Missing colon → SyntaxError

How to fix: Correct the syntax.

4.2 Logical Errors (Semantic Errors)

The code runs without crashing, but produces incorrect results.

Key Characteristics:

Example:

numbers = [10, 20, 30] average = sum(numbers) # Forgot to divide by len(numbers) print(f"The average is: {average}") # Prints 60 instead of 20

How to fix: Debugging – trace through logic, use print statements, or use a debugger.

4.3 Runtime Errors (Exceptions)

These occur while the program is running, when an unexpected condition is encountered.

Key Characteristics:

Example:

x = 10 y = 0 result = x / y # ZeroDivisionError

Summary Table:

Error Type Detection Time Program Execution Example
Syntax Error Before execution (parsing) Does not start print("Hello" (missing parenthesis)
Logical Error During execution Runs, but wrong results average = sum(numbers) instead of sum/len
Runtime Error During execution Runs until the error, then crashes 10 / 0

5. What is an Exception?

An exception is an event that disrupts the normal flow of the program's instructions. In Python, exceptions are objects.

5.1 The Exception Lifecycle

  1. Raising: When an error is detected, Python creates an exception object and raises it (automatically or with raise).
  2. Propagation: The exception travels up the call stack until it finds a handler.
  3. Handling (Catching): If an appropriate try...except block is found, the program can recover.
  4. Unhandled: If no handler is found, the program terminates with a traceback.

5.2 Common Causes of Exceptions

5.3 The Traceback: Your Debugging Friend

When an exception is unhandled, Python prints a traceback – a detailed report.

Example traceback:

Traceback (most recent call last): File "program.py", line 8, in <module> result = divide_numbers(10, 0) File "program.py", line 4, in divide_numbers return a / b ZeroDivisionError: division by zero

What the traceback tells you:

6. Python's Exception Hierarchy

All exceptions inherit from a base class. Understanding this hierarchy helps you write more precise handlers.

6.1 Conceptual Overview

BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit └── Exception ├── ArithmeticError │ └── ZeroDivisionError ├── LookupError │ ├── IndexError │ └── KeyError ├── OSError │ ├── FileNotFoundError │ └── PermissionError ├── NameError ├── TypeError ├── ValueError └── ... (many more)

6.2 Important Points

6.3 Using the Hierarchy

You can catch a more general exception to handle a family of errors:

try: result = 10 / 0 except ZeroDivisionError: print("Specific: Cannot divide by zero") except ArithmeticError: print("General: Some arithmetic error")

Best practice: place more specific exceptions first.

7. Why Handle Exceptions?

Handling exceptions is about writing professional, robust software.

7.1 Benefits

  1. Prevents crashes – the program can recover gracefully.
  2. Provides meaningful error messages – user‑friendly, not cryptic tracebacks.
  3. Separates error‑handling code from normal business logic.
  4. Improves robustness – handles unexpected conditions.
  5. Facilitates debugging – you can log detailed information for developers.

7.2 Example: With vs. Without Handling

Without handling (crashes):

filename = input("Enter filename: ") file = open(filename, "r") # FileNotFoundError if file missing

With handling (recovers):

try: filename = input("Enter filename: ") with open(filename, "r") as file: content = file.read() except FileNotFoundError: print("File not found. Please check the name.") except Exception as e: print(f"Unexpected error: {e}") finally: print("Thank you for using the file reader.")

8. Key Takeaways and Summary

Concept Description
Syntax Error Code violates grammar; program cannot run.
Logical Error Code runs but gives wrong results; no error message.
Runtime Exception Code runs until an error occurs; crashes unless handled.
Traceback Shows call stack and error details for debugging.
Exception Hierarchy BaseException → Exception → specific exceptions. Use specificity to catch appropriately.
Why Handle? Prevents crashes, provides clear messages, separates concerns, improves robustness.

9. Self‑Assessment Quiz

Test your understanding of the concepts covered in this tutorial. Answer each question, then click to reveal the correct answer.

Q1: What type of error occurs when the Python interpreter cannot parse your code?

Answer(C) Syntax Error

Q2: What is the output of the following code?

numbers = [1, 2, 3] total = sum(numbers) print(total / 0)
Answer(C) A `ZeroDivisionError` exception

Q3: True or False: A logical error will cause the program to crash with an error message.

AnswerFalse. A logical error runs without crashing but produces incorrect results.

Q4: Which of the following is the base class for all built‑in, non‑system‑exiting exceptions in Python?

Answer(B) `Exception`

Q5: What is a traceback used for?

Answer(B) To debug and understand where an exception occurred

Q6: Which of the following is NOT a benefit of exception handling?

Answer(C) Makes the code run faster

Q7: Consider the exception hierarchy. Which of the following catches both ZeroDivisionError and OverflowError?

Answer(B) `except ArithmeticError:`

Q8: Given the following code, what will be printed?

try: x = int("abc") except ValueError: print("Value error caught!") except TypeError: print("Type error caught!") except Exception: print("General exception caught!")
Answer(A) Value error caught!

10. Practical Exercises

Complete the exercises below to reinforce your understanding. Sample solutions are provided after each exercise.

Exercise 1: Identifying Error Types

Instructions: For each of the following code snippets, identify what type of error or exception would occur and explain why.

a)

for i in range(5) print(i)
Sample AnswerSyntax Error. The `for` loop is missing a colon `:` at the end of the line.

b)

def calculate_interest(principal, rate, time): return principal * rate * time / 100 result = calculate_interest(1000, 5, 2) print("The interest is:", result) # Expected output: 100, but it prints 100.0
Sample AnswerLogical Error. The code runs but produces `100.0` instead of `100` because `/` returns a float. A fix would be to use integer division `//` or format the output.

c)

my_list = [1, 2, 3] print(my_list[5])
Sample AnswerRuntime Exception (`IndexError`). Index 5 is out of range for a list with indices 0, 1, 2.

d)

student = {"name": "Alice", "age": 25} print(student["grade"])
Sample AnswerRuntime Exception (`KeyError`). The key `"grade"` does not exist in the dictionary.

Exercise 2: Reading and Interpreting Tracebacks

Instructions: Consider the following code and traceback. Answer the questions below.

# File: calculator.py def divide(a, b): return a / b def average(numbers): total = sum(numbers) count = len(numbers) result = divide(total, count) return result data = [10, 20, 0, 30] print(average(data))

Traceback:

Traceback (most recent call last): File "calculator.py", line 11, in <module> print(average(data)) File "calculator.py", line 7, in average result = divide(total, count) File "calculator.py", line 2, in divide return a / b ZeroDivisionError: division by zero

Questions:

  1. What type of exception occurred?
  2. At what line number did the exception actually occur (the line that caused it)?
  3. What function called the function that caused the error?
  4. What specific value caused the error?
Sample Answer 1. `ZeroDivisionError`
2. Line 2 (inside the `divide` function)
3. The `average` function called `divide`.
4. The `count` variable is 0 because the list is empty. (If the list were `[]`, `len(numbers)` would be 0, causing division by zero. In the given traceback, the list `[10, 20, 0, 30]` has length 4, so no error occurs; the traceback would be different. For the traceback to make sense, assume `data = []`.)

Exercise 3: Adding Exception Handling

Instructions: Rewrite the following code to handle:

Original Code:

numerator = int(input("Enter the numerator: ")) denominator = int(input("Enter the denominator: ")) result = numerator / denominator print(f"Result: {result}")
Sample Answer
def safe_divide(): try: numerator = int(input("Enter the numerator: ")) denominator = int(input("Enter the denominator: ")) result = numerator / denominator print(f"Result: {result}") except ValueError: print("Error: Please enter valid numeric values.") except ZeroDivisionError: print("Error: Cannot divide by zero.") except Exception as e: print(f"An unexpected error occurred: {e}") safe_divide()

Exercise 4: Understanding the Exception Hierarchy

Instructions: Given the exception hierarchy snippet below, determine which except block(s) would catch the exception in each scenario.

Exception ├── ArithmeticError │ └── ZeroDivisionError ├── LookupError │ ├── IndexError │ └── KeyError └── ValueError

a)

try: print([1, 2, 3][5]) except IndexError: print("Index error caught") except LookupError: print("Lookup error caught") except Exception: print("General exception caught")
Sample Answer`IndexError` caught by the first `except` block. The `LookupError` block is not reached.

b)

try: int("abc") except LookupError: print("Lookup error caught") except ValueError: print("Value error caught") except Exception: print("General exception caught")
Sample Answer`ValueError` caught by the second `except`. The `LookupError` block is irrelevant; `Exception` not reached.

c)

try: 10 / 0 except Exception: print("General exception caught") except ZeroDivisionError: print("Zero division caught")
Sample Answer`ZeroDivisionError` is raised. The first `except Exception` matches because `ZeroDivisionError` is a subclass of `Exception`, so "General exception caught" is printed. The `ZeroDivisionError` block is never reached because the order is incorrect (more specific should come first).

11. Homework Questions

Answer the following questions in complete sentences. For essay questions, aim for 300–500 words. Sample answers are provided after each question.

Short Answer Questions

1. Explain the difference between syntax errors, logical errors, and runtime exceptions. For each type, provide a real‑world scenario (not code) that illustrates the concept, and then give a Python code example.

Sample Answer **Syntax Error:** Like trying to follow a recipe written in a language you don't understand – the grammar is wrong, so you can't even start. In Python, a missing colon or parenthesis is a syntax error. *Code Example:* `for i in range(10) print(i)` (missing colon).

Logical Error: Like following a recipe that tells you to add sugar instead of salt – the instructions are syntactically correct, but the outcome is wrong.
Code Example: average = sum(numbers) instead of sum(numbers) / len(numbers).

Runtime Exception: Like a power outage while you're cooking – the recipe is correct, but an external factor interrupts the process. In Python, dividing by zero or opening a missing file causes a runtime exception.
Code Example: x = 10 / 0.

2. The following program is supposed to read numbers from "data.txt", calculate their average, and print it. However, it contains multiple errors. Identify and fix all errors.

data_file = open(data.txt, "r") lines = data_file.readlines() total = 0 count = 0 for line in lines number = int(line) total += number count = count + 1 average = total / count print("The average is: average) data_file.close()
Sample Answer

Errors:

  1. data.txt is not quoted as a string.
  2. The for loop is missing a colon.
  3. line may contain newline characters; use .strip().
  4. If count is zero, division by zero occurs.
  5. The average variable is not correctly inserted into the print string.

Fixed code:

try: data_file = open("data.txt", "r") except FileNotFoundError: print("Error: data.txt not found.") exit() lines = data_file.readlines() data_file.close() total = 0 count = 0 for line in lines: try: number = int(line.strip()) total += number count += 1 except ValueError: print(f"Warning: Skipping invalid line: {line.strip()}") if count == 0: print("No valid numbers found.") else: average = total / count print(f"The average is: {average}")

3. Write a Python function get_valid_integer(prompt, min_value, max_value) that repeatedly asks the user for an integer within the given range, handles ValueError and any other exception, and returns the valid integer.

Sample Answer
def get_valid_integer(prompt, min_value, max_value): while True: try: user_input = input(prompt) value = int(user_input) if value < min_value or value > max_value: print(f"Error: Value must be between {min_value} and {max_value}.") continue return value except ValueError: print("Error: Please enter a valid integer.") except Exception as e: print(f"An unexpected error occurred: {e}")

4. Given the following code, write the traceback that would be generated if the user enters "abc" for age, then explain what the traceback tells you, and finally modify the code to handle the exception gracefully.

def get_user_data(): name = input("Enter name: ") age = int(input("Enter age: ")) return name, age def display_user(): user_name, user_age = get_user_data() print(f"User: {user_name}, Age: {user_age}") def main(): display_user() if __name__ == "__main__": main()
Sample Answer

Traceback:

Traceback (most recent call last): File "program.py", line 14, in <module> main() File "program.py", line 10, in main display_user() File "program.py", line 6, in display_user user_name, user_age = get_user_data() File "program.py", line 3, in get_user_data age = int(input("Enter age: ")) ValueError: invalid literal for int() with base 10: 'abc'

Explanation: The error occurred in get_user_data at line 3. int() tried to convert 'abc' to an integer, which failed. The call stack shows main() → display_user() → get_user_data(). The error type is ValueError.

Modified code to handle gracefully:

def get_user_data(): name = input("Enter name: ") try: age = int(input("Enter age: ")) except ValueError: print("Invalid age. Please enter a number.") return get_user_data() # retry recursively return name, age

Essay Question

5. Discuss the role of exception handling in software development. Include the importance of graceful error recovery, the balance between catching early vs. letting exceptions propagate, when exceptions should be used for control flow (and when not), and the trade‑offs between try...except and conditional checks.

Sample Answer

Exception handling is a cornerstone of professional software development. It allows programs to manage errors gracefully, preventing crashes and providing meaningful feedback to users. Without exception handling, a single missing file or invalid input could terminate an entire application, leading to a poor user experience.

One key decision is whether to catch exceptions early or let them propagate. The principle is to catch exceptions at a level where you can meaningfully handle them. Low‑level functions should not catch exceptions they cannot resolve; instead, they should let them propagate to higher layers where the appropriate recovery strategy is known. For example, a file‑reading function might catch FileNotFoundError and attempt to create a default file, while a higher‑level function might catch a broader RuntimeError and log it for debugging.

When it comes to using exceptions for control flow, the consensus is clear: exceptions should be used for exceptional conditions, not for normal program flow. Using try...except to check if a key exists in a dictionary instead of using get() or if key in dict is considered an antipattern because exceptions are expensive to raise and make the code harder to read.

The trade‑off between try...except and conditional checks depends on the situation. For rare, truly exceptional conditions (e.g., file not found when it should exist), try...except keeps the normal code path clean and separates error handling from business logic. For common, expected conditions (e.g., checking if a list is empty before accessing an element), conditionals are clearer and more performant.

In summary, good exception handling requires judgment: use try...except for recoverable errors in production, reserve assert for debugging, and rely on conditionals for anticipated states. This balance yields code that is both robust and maintainable.

Research Question

6. Research the exception hierarchy in Python. What are the key differences between BaseException and Exception? Why is it generally considered bad practice to catch BaseException or use a bare except: statement? Provide examples of when you might actually want to catch BaseException.

Sample Answer

The Python exception hierarchy is rooted in BaseException, from which all exceptions derive. BaseException includes system‑level exceptions like SystemExit (raised by sys.exit()) and KeyboardInterrupt (raised when the user presses Ctrl+C). Exception, on the other hand, is the base class for all built‑in, non‑system‑exiting exceptions. Most user‑defined exceptions should inherit from Exception.

Catching BaseException or using a bare except: is generally bad practice because it intercepts system‑level exceptions that are not meant to be caught. For example, catching KeyboardInterrupt prevents a user from terminating a stuck program, and catching SystemExit prevents a program from exiting cleanly. These exceptions are designed to control the program's lifecycle and should be allowed to propagate.

There are rare cases where catching BaseException is appropriate, such as when writing a system service that must clean up resources before exiting, or when implementing a debugger that needs to log all exceptions. However, in almost all cases, catching Exception is the correct choice, and catching BaseException should be reserved for specialized system‑level programming.

Previous | Tutorial index | Next