How programs handle unexpected conditions gracefully — tracebacks, try and except, validation loops, file errors, raising exceptions, and designing programs that fail safely
Every program eventually meets a situation it did not expect. A user types a word where the program expects a number. A file that the program needs is missing. A calculation tries to divide by zero. A network connection drops. A list index turns out to be out of range.
In the early chapters of this book, those situations crashed programs. A traceback appeared on the screen, the program stopped, and the user saw an error message they probably did not understand. That is acceptable when you are learning. Tracebacks are useful feedback during development. But a finished program — one you give to another person, run automatically, or rely on for real work — should not crash every time something unexpected happens. It should handle the problem gracefully, explain what went wrong in plain language, and either recover or stop cleanly.
This chapter teaches you how to do that. Error handling is not an advanced topic that you add to a program at the end. It is part of program design. A program that handles errors well is a program that respects its users and its own data.
A program asks the user to enter a score between 0 and 100. The user types "ninety-five". What should happen? The program could crash with a ValueError traceback. It could silently store a wrong value. Or it could print a clear message and ask again. Only one of those responses is good program design. By the end of this chapter, you will know how to build programs that always take the third path — handling the unexpected without crashing, without hiding problems, and without confusing the user.
By the end of this chapter, you should be able to:
try/except statement that catches a specific exception and responds to it appropriately.try statement with multiple except clauses that handle different exception types differently.else and finally clauses of a try statement and explain when each one runs.FileNotFoundError and PermissionError.raise statement to signal that a function cannot complete its task.assert appropriately as a development tool for internal consistency checks.try StatementBasic try/except structure; how Python decides which block to execute; tracing execution through a try statementexceptCatching specific exception types; the as keyword; accessing the exception object; why bare except is usually too broadelse and finallyThe else clause (runs only on success); the finally clause (runs always); cleanup use cases; tracing all four clauses togetherPython programs run as a sequence of instructions. Most of the time, each instruction succeeds and execution moves to the next one. But sometimes an instruction cannot complete because of a condition the program did not anticipate.
Consider this short program:
age = int(input("Enter your age: "))
print("In ten years you will be", age + 10)
This works correctly when the user types a number. But what happens if they type "twenty"?
The program stopped immediately. The second line never ran. The user received a message that is technically accurate but not particularly friendly.
This is an exception: a signal that something went wrong during execution that Python does not know how to handle on its own. When Python raises an exception and your code does not handle it, the program terminates and prints a traceback.
Exceptions are not the same as bugs, although a bug can cause one. An exception can be caused by:
Many of these situations are not the programmer's fault and cannot be entirely prevented. A user can always type unexpected input. A file can always be missing. The right response is not to prevent these events entirely — that is often impossible — but to handle them when they occur.
An exception is a signal that something unexpected happened. Handling an exception means deciding what your program should do when it receives that signal. A well-designed program handles exceptions gracefully — giving the user a clear message and continuing or exiting cleanly — rather than crashing with a raw traceback.
When Python encounters an error it cannot handle, it raises an exception and prints a traceback: a report showing where the problem occurred and what kind of problem it was. Learning to read tracebacks is one of the most important debugging skills you can develop.
Here is an example program:
def calculate_average(scores):
total = sum(scores)
count = len(scores)
return total / count
results = []
average = calculate_average(results)
print("Average:", average)
Traceback (most recent call last): — Python shows the chain of function calls that led to the error, starting from the outermost call and ending at the line that actually failed.File "average.py", line 7, in <module> — the program called calculate_average(results) on line 7, at the top level of the script.File "average.py", line 4, in calculate_average — inside that function, line 4 is where the problem occurred.return total / count — this is the exact line that failed.ZeroDivisionError: division by zero — the exception type and a plain-English description. This is always the most useful part. Read it first.Start at the bottom of a traceback. The exception type and message tell you what went wrong. Then work upward to find where it happened. The line at the bottom of the call chain is the immediate source of the error; the lines above it show how execution got there.
| Exception | When it occurs | Example |
|---|---|---|
ValueError | Right type, wrong value | int("hello") |
TypeError | Operation on wrong type | "5" + 3 |
ZeroDivisionError | Division or modulo by zero | 10 / 0 |
IndexError | List index out of range | items[99] when list has 5 items |
KeyError | Dictionary key does not exist | d["missing"] |
FileNotFoundError | File not found at given path | open("no_such_file.txt") |
NameError | Variable or function not defined | print(undefined_var) |
AttributeError | Object lacks the attribute or method | "hello".push("!") |
ImportError | Module cannot be imported | import nonexistent_module |
PermissionError | OS refuses the file operation | Writing to a read-only file |
try StatementPython handles exceptions with the try statement. The basic idea is this: you mark a block of code that might fail, and you provide a separate block of code that runs only if it does fail.
try:
# Code that might raise an exception
except:
# Code that runs if an exception occurs
Here is the age-input example from Section 23.1, rewritten with a try statement:
try:
age = int(input("Enter your age: "))
print("In ten years you will be", age + 10)
except:
print("That doesn't look like a valid age. Please enter a number.")
The program no longer crashes. Here is what Python does when it encounters a try statement:
try block.try block succeeds, Python skips the except block entirely.try block and jumps to the except block.except block finishes, execution continues after the entire try/except structure.When an exception occurs inside a try block, the remaining lines in that block do not run. Execution jumps directly to except. This means any code that depends on earlier lines in the try block will not have access to values that were never assigned.
try:
x = int("abc") # This will fail
print("Success!") # This will not run
except:
print("Something went wrong.")
print("Program continues here.")
| Step | Line | What happens |
|---|---|---|
| 1 | x = int("abc") | Raises ValueError. Python jumps immediately to except. |
| 2 | print("Success!") | Skipped — never runs. |
| 3 | print("Something went wrong.") | The except block runs. |
| 4 | print("Program continues here.") | Execution resumes after the try/except structure. |
exceptThe bare except: clause from Section 23.3 catches every exception without distinguishing between them. This is often too broad. Catching all exceptions silently can hide real bugs — if your program has a logic error that raises an unexpected exception, a bare except clause would swallow that error and make it very hard to find.
The better practice is to name the specific exception type you want to handle:
try:
age = int(input("Enter your age: "))
print("In ten years you will be", age + 10)
except ValueError:
print("Please enter a whole number.")
This except ValueError: clause catches only ValueError exceptions. If a different, unexpected exception occurs, it will still propagate as a traceback — which is usually what you want during development. Unexpected problems should be visible, not hidden.
You can capture the exception itself using the as keyword. This gives you access to the error message Python generated:
try:
number = int(input("Enter a number: "))
result = 100 / number
print("Result:", result)
except ValueError as error:
print("Input error:", error)
except ZeroDivisionError as error:
print("Math error:", error)
The variable after as (here, error) holds the exception object. Printing it produces Python's built-in description of the problem. You do not always need to print error directly — sometimes you will write your own message — but having access to it is useful for logging or giving more specific feedback.
error, e, and exc are all common names for a caught exception. Any valid variable name works. Choose one that reads naturally in context. In production code, many programmers use e for brevity; in teaching examples, error is more readable.
A try statement can have multiple except clauses, each handling a different exception type. Python checks each clause in order and runs the first one that matches.
def safe_divide(numerator, denominator):
try:
result = numerator / denominator
return result
except TypeError:
print("Error: Both values must be numbers.")
return None
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
return None
print(safe_divide(10, 2)) # Works: 5.0
print(safe_divide(10, 0)) # ZeroDivisionError
print(safe_divide(10, "two")) # TypeError
If you want to handle several exception types the same way, list them in a tuple:
try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError):
print("Please enter a non-zero whole number.")
Python exceptions are organized in a hierarchy. Every exception is a subclass of Exception. Catching a parent class catches all of its children. For example, both ValueError and TypeError are subclasses of Exception. If you write except Exception:, you catch nearly all exceptions — more specific than bare except:, but still quite broad.
As a beginner, catch specific exception types whenever you can. It makes your code clearer, makes each handler's purpose obvious, and prevents accidentally hiding bugs that raise exception types you did not anticipate.
else and finallyThe try statement has two optional additional clauses: else and finally. Together, the full structure is:
try:
# Code that might fail
except SomeException:
# Runs if SomeException occurs
else:
# Runs only if no exception occurred
finally:
# Runs no matter what
The else block runs only if the try block completed without raising any exception. It is useful for code that should run after a successful operation but that you do not want inside the try block itself — because any exception from that code would also be caught, possibly hiding a bug.
try:
number = int(input("Enter a number: "))
except ValueError:
print("That is not a valid number.")
else:
print("You entered:", number)
print("Its square is:", number ** 2)
If the input is valid, else runs and shows the results. If a ValueError occurs, only the except block runs — else is skipped entirely.
The finally block runs regardless of whether an exception occurred, whether it was caught, or whether else ran. It always executes. The most common use for finally is cleanup: closing a file, releasing a resource, or resetting something to a known state.
def read_config(filename):
file_handle = None
try:
file_handle = open(filename, "r")
content = file_handle.read()
return content
except FileNotFoundError:
print(f"Config file '{filename}' not found.")
return None
finally:
if file_handle is not None:
file_handle.close()
print("File closed.")
In most modern Python file-handling code, the with statement (Chapter 21) handles file cleanup automatically. But finally remains important for other kinds of cleanup — database connections, network sockets, temporary files, or any resource that must be explicitly released.
try succeeds → except is skipped → else runs (prints word count) → finally runs (prints completion message)try raises FileNotFoundError → except runs (prints error) → else is skipped → finally runs (prints completion message)try:
filename = input("Enter filename: ")
with open(filename, "r") as file:
data = file.read()
except FileNotFoundError:
print(f"File '{filename}' was not found.")
else:
word_count = len(data.split())
print(f"File contains {word_count} words.")
finally:
print("File operation complete.")
In Chapter 8 you used while loops to keep asking for input until the user provided an acceptable value. Exception handling gives you a better tool: you can attempt the conversion inside a try block and repeat the loop only if it fails.
while True:
user_input = input("Enter a whole number: ")
try:
number = int(user_input)
break
except ValueError:
print("That is not a whole number. Please try again.")
print("You entered:", number)
| Input | What happens |
|---|---|
"hello" | int("hello") raises ValueError. except prints message. break not reached. Loop repeats. |
"42" | int("42") succeeds. break exits the loop. |
This pattern — a while True loop that breaks only on success — is one of the most useful patterns in practical Python programs.
while True:
user_input = input("Enter a score between 0 and 100: ")
try:
score = int(user_input)
if score < 0 or score > 100:
print("Score must be between 0 and 100.")
else:
break
except ValueError:
print("Please enter a whole number.")
print("Score accepted:", score)
This pattern appears often enough that it is worth turning into a function with optional minimum and maximum bounds:
def get_integer(prompt, minimum=None, maximum=None):
"""Ask for an integer, repeating until valid input is given.
Optional minimum and maximum bounds are enforced if provided.
"""
while True:
user_input = input(prompt)
try:
value = int(user_input)
if minimum is not None and value < minimum:
print(f"Please enter a value of at least {minimum}.")
elif maximum is not None and value > maximum:
print(f"Please enter a value of at most {maximum}.")
else:
return value
except ValueError:
print("Please enter a whole number.")
age = get_integer("Enter your age: ", minimum=0, maximum=150)
score = get_integer("Enter your score (0-100): ", minimum=0, maximum=100)
quantity = get_integer("How many items? ", minimum=1)
Each call handles its own validation loop. The rest of the program receives clean, validated data and does not need to check again.
File operations are among the most common sources of exceptions in practical programs. Files can be missing, in the wrong location, in the wrong format, or protected by the operating system.
filename = input("Enter the filename to read: ")
try:
with open(filename, "r") as file:
content = file.read()
print("File contents:")
print(content)
except FileNotFoundError:
print(f"Error: The file '{filename}' was not found.")
print("Check that the filename and path are correct.")
try:
with open("protected_file.txt", "w") as file:
file.write("New content")
except PermissionError:
print("Error: You do not have permission to write to that file.")
def read_file_safely(filename):
"""Read a file and return its contents, or None if unsuccessful."""
try:
with open(filename, "r") as file:
return file.read()
except FileNotFoundError:
print(f"Error: '{filename}' does not exist.")
return None
except PermissionError:
print(f"Error: Cannot read '{filename}' — permission denied.")
return None
except OSError as error:
print(f"Error reading '{filename}': {error}")
return None
OSError is a broader category that covers many file system problems. Catching it as a fallback after more specific exceptions is a useful defensive pattern.
The pathlib module lets you check whether a path exists before trying to open it. Both approaches are valid:
Path(filename).exists() to test the path first. Clear and explicit. However, a file could be deleted between the check and the open() call in rare concurrent scenarios.open() and catch FileNotFoundError if it fails. Handles the file-disappearing edge case automatically. Many programmers prefer this for robustness.def load_scores(filename):
"""Load a list of scores from a text file, one per line."""
scores = []
try:
with open(filename, "r") as file:
for line_number, line in enumerate(file, start=1):
line = line.strip()
if line == "":
continue
try:
score = int(line)
scores.append(score)
except ValueError:
print(f"Warning: Line {line_number} contains '{line}', "
f"which is not a number. Skipping.")
except FileNotFoundError:
print(f"Error: Score file '{filename}' not found.")
return None
return scores
filename = "scores.txt"
data = load_scores(filename)
if data is not None:
print(f"Loaded {len(data)} scores.")
if len(data) > 0:
print(f"Average: {sum(data) / len(data):.1f}")
FileNotFoundError if the file does not exist. Returns None — the caller knows the operation failed completely.ValueError for individual lines that cannot be converted. Prints a warning and skips that line — the rest of the file is still processed.So far, you have only caught exceptions that Python raises. But you can also raise exceptions yourself using the raise statement. This is useful when your function receives input that is technically valid Python but does not meet your program's requirements. Instead of silently continuing with bad data, you signal that something is wrong — the same way Python signals errors to you.
def calculate_square_root(number):
if number < 0:
raise ValueError("Cannot calculate the square root of a negative number.")
return number ** 0.5
If you call calculate_square_root(-4), Python raises:
The calling code can then handle this ValueError with a try/except block:
try:
result = calculate_square_root(float(input("Enter a number: ")))
print(f"Square root: {result:.4f}")
except ValueError as error:
print(f"Error: {error}")
None or -1 to signal failure. Works, but the caller must remember to check. Easy to forget and silently use the wrong value.def parse_positive_integer(text):
"""Convert text to a positive integer, or raise ValueError."""
try:
value = int(text)
except ValueError:
raise ValueError(f"'{text}' is not a whole number.")
if value <= 0:
raise ValueError(f"{value} is not a positive number.")
return value
This function catches Python's built-in ValueError from int() and raises a new one with a clearer, more specific message. This is called re-raising with a better message.
def calculate_average(scores):
assert len(scores) > 0, "Cannot average an empty list."
return sum(scores) / len(scores)
The assert statement checks that a condition is true, and raises AssertionError if it is not. Assertions are typically used during development to catch programming mistakes — cases where you made an incorrect assumption about your data.
Use raise ValueError (or another appropriate exception) for user-facing validation — when bad data arrives from user input, files, or external sources. Use assert for internal consistency checks during development — to verify assumptions about your own code. Assertions can be disabled when Python runs with the -O flag, so they should never be the only line of defense against bad data in production programs.
Exception handling is not just a technical feature. It is a design practice. A program that handles errors well is one that has been designed with realistic conditions in mind — conditions where users make mistakes, files go missing, and data turns out to be imperfect.
except: pass is almost always wrong. It hides problems and lets the program continue with missing or corrupted data. Silent failures are worse than crashes because the problem is invisible.None (on failure) rather than an empty result that is indistinguishable from a legitimate empty result. Let callers check the return value.This is dangerous code: except: pass. It swallows all exceptions without any record that something went wrong. The program continues, but it may have missing or corrupted data. Always at minimum print or log a message when catching an exception.
Here is a complete small program that applies all five principles. It reads a list of numbers from a file chosen by the user, computes statistics, and handles every foreseeable problem.
def get_filename():
"""Ask the user for a filename and return it."""
return input("Enter the data file to analyze: ").strip()
def load_numbers(filename):
"""Load numbers from a file. Return a list or None on failure."""
numbers = []
try:
with open(filename, "r") as file:
for line_number, line in enumerate(file, start=1):
line = line.strip()
if line == "":
continue
try:
numbers.append(float(line))
except ValueError:
print(f" Warning: Line {line_number} ('{line}') "
f"is not a number. Skipping.")
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
return None
except PermissionError:
print(f"Error: Cannot read '{filename}'. Permission denied.")
return None
return numbers
def compute_statistics(numbers):
"""Compute basic statistics. Raises ValueError if list is empty."""
if len(numbers) == 0:
raise ValueError("No numbers to analyze.")
return {
"count": len(numbers),
"total": sum(numbers),
"average": sum(numbers) / len(numbers),
"minimum": min(numbers),
"maximum": max(numbers),
}
def display_statistics(stats):
"""Display statistics to the user."""
print()
print("=== Statistics ===")
print(f"Count: {stats['count']}")
print(f"Total: {stats['total']:.2f}")
print(f"Average: {stats['average']:.2f}")
print(f"Minimum: {stats['minimum']:.2f}")
print(f"Maximum: {stats['maximum']:.2f}")
def main():
filename = get_filename()
numbers = load_numbers(filename)
if numbers is None:
print("Could not read the file. Exiting.")
return
try:
stats = compute_statistics(numbers)
display_statistics(stats)
except ValueError as error:
print(f"Error: {error}")
main()
load_numbers handles file-level errors; compute_statistics raises ValueError and lets main handle it. Each function handles what it knows about.except clause prints a message. Nothing is swallowed silently.load_numbers returns None on file failure (distinguishable from an empty file) and a list (possibly empty) on success. main checks for None before proceeding.compute_statistics, they are known to be valid floats.ValueError, TypeError, ZeroDivisionError, IndexError, KeyError, and FileNotFoundError.try statement marks code that might fail. If an exception occurs inside the try block, the rest of that block is skipped and the except block runs. Execution then continues after the entire structure.except clause (such as except ValueError:) is better than a bare except:. Use as error to capture the exception object and access its message.except clauses handle different exception types differently. Several types can be listed in a tuple to be handled the same way. Catch the most specific exceptions you can.else clause runs only if no exception occurred. The finally clause runs regardless. Use else to separate successful-path code from the risky code in try; use finally for cleanup that must happen in all cases.while True with a try/except block that breaks only on success. This pattern can be packaged into a reusable function like get_integer that accepts optional bounds.FileNotFoundError, PermissionError, and OSError. Nested try/except blocks allow different responses to file-level failures and individual-line failures.raise statement signals that a function cannot complete its task. Use it when a function receives technically valid Python but logically invalid data. Use assert for internal consistency checks during development, not for user-facing validation.except block.try block.try statement that runs only if no exception occurred in the try block. Used to separate success-path code from risky code.try statement that runs regardless of whether an exception occurred or was caught. Used for cleanup operations.int("hello").AssertionError if the condition is False. Used to verify internal assumptions during development, not for user-facing validation.Study each example carefully without running it. Read it completely, then answer the questions.
def get_positive_number(prompt):
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Please enter a number greater than zero.")
else:
return value
except ValueError:
print("Please enter a numeric value.")
radius = get_positive_number("Enter the radius: ")
area = 3.14159 * radius ** 2
print(f"Area: {area:.2f}")
"five" as the radius?-3 as the radius?0?7.5?return value instead of break?def safe_read_line(filename, line_number):
try:
with open(filename, "r") as file:
lines = file.readlines()
if line_number < 1 or line_number > len(lines):
return None
return lines[line_number - 1].strip()
except FileNotFoundError:
return None
result = safe_read_line("notes.txt", 3)
if result is None:
print("Could not retrieve the line.")
else:
print("Line 3:", result)
notes.txt exists and has 5 lines, what does safe_read_line("notes.txt", 3) return?notes.txt exists and has 5 lines, what does safe_read_line("notes.txt", 10) return?notes.txt does not exist, what does the function return?line_number when indexing lines. Why?Trace the following programs by writing the complete output for each.
def convert(text):
try:
return int(text)
except ValueError:
try:
return float(text)
except ValueError:
return None
values = ["42", "3.14", "hello", "100", ""]
for item in values:
result = convert(item)
print(f"'{item}' -> {result}")
numbers = [10, 0, 5, 0, 2]
results = []
for n in numbers:
try:
result = 100 / n
results.append(result)
except ZeroDivisionError:
results.append(None)
print(results)
results?def process(data):
try:
value = int(data)
print("Converted:", value)
except ValueError:
print("Not an integer.")
else:
print("Success!")
finally:
print("Done.")
process("42")
print("---")
process("abc")
"Success!" print for both calls, only one, or neither? Explain why.For each code block, predict exactly what will be printed. If an unhandled exception will occur, write the exception type and explain why.
try:
x = 10 / 2
y = int("5")
except ZeroDivisionError:
print("Division error")
except ValueError:
print("Value error")
else:
print(x + y)
items = ["apple", "banana", "cherry"]
try:
print(items[5])
except IndexError:
print("Index out of range")
print("Continuing")
def mystery(n):
try:
result = 100 // n
except ZeroDivisionError:
return -1
return result
print(mystery(4))
print(mystery(0))
print(mystery(10))
data = {"name": "Jordan", "age": 19}
try:
print(data["score"])
except KeyError:
print("Key not found")
finally:
print("Lookup complete")
Each program has a problem. Identify the error, explain what goes wrong, and write a corrected version.
try:
number = int(input("Enter a number: "))
print("Double:", number * 2)
except:
pass
except with pass? What kinds of errors will this hide? Write a corrected version that catches ValueError specifically and prints a helpful message.while True:
entry = input("Enter score (0-100): ")
score = int(entry)
if 0 <= score <= 100:
break
print("Out of range. Try again.")
"ninety"? Fix the program so it handles this gracefully without crashing.def read_number_file(filename):
try:
with open(filename) as file:
numbers = [int(line) for line in file]
return numbers
except:
return []
FileNotFoundError and ValueError separately, with appropriate messages for each.def safe_sqrt(x):
import math
try:
return math.sqrt(x)
except ValueError:
print("Cannot take square root of negative number.")
x is negative? Why is that a problem for callers? Rewrite the function so its behavior is clear and callers can easily detect failure.get_float(prompt) that asks for a floating-point number, repeats until a valid number is entered, and returns the float.safe_get(items, index) that returns items[index] if the index is valid, or None if it is out of range. Use a condition, not try/except.lookup(dictionary, key, default="N/A") that returns the value for key if it exists, or default if it does not. Handle KeyError with try/except.count_lines(filename) that returns the number of lines in a text file, or -1 if the file cannot be opened.parse_numbers(text) that splits a string on spaces, converts each word to an integer, and returns the successful conversions as a list. Words that cannot be converted should be skipped with a printed warning.append_to_log(filename, message) that appends message as a new line. Return True on success, or print an error and return False if a PermissionError occurs.Build a program called robust_calculator.py that performs basic arithmetic on values entered by the user. The program should be robust against all common input problems.
Offer a menu: addition, subtraction, multiplication, division, and quit. For each operation, ask for two numbers and display the result. The program must handle all of the following without crashing: non-numeric input for either number; division by zero; invalid menu choices; KeyboardInterrupt and EOFError. Continue running after any error, repeating the menu until the user quits.
get_number(prompt) — asks for a number, validates it, returns a float.get_menu_choice() — asks for a menu choice, validates it.calculate(operation, a, b) — performs the chosen operation, returns the result or None on failure.display_menu() — prints the available operations.main() — runs the main program loop.Add a calculation history. Keep a list of every successful calculation during the session. Add a menu option to display it. When the user quits, save the history to calculator_history.txt. Handle all file errors gracefully.
These questions draw on skills from earlier chapters as well as Chapter 23.
safe_average(numbers). If the list is empty, raise ValueError. If the list contains non-numeric values, skip them with a warning. Return the average of the valid numbers, or raise ValueError if none remain.load_student_records(filename) that returns a list of dictionaries with "name" and "score" keys. Handle: the file not existing, lines with the wrong number of fields, and scores that are not valid integers. Print warnings for malformed lines.if len(items) > 0: before accessing items[0]) and Program B (using try: print(items[0]) and catching IndexError). In what situations would each be preferable?try block. Identify what can go wrong and what cannot, then rewrite the program so that only the risky operations are inside the try block:try:
a = int(input("Enter a: "))
b = int(input("Enter b: "))
c = a / b
d = a + b
e = a * b
print(a, b, c, d, e)
except:
print("An error occurred.")except: clause is usually too broad.else and finally clauses.raise versus assert.try/except statement that catches a specific exception.try statement with multiple except clauses for different errors.FileNotFoundError and PermissionError in file operations.raise ValueError to signal invalid input inside a function.Chapter 24 introduces two more standard-library tools: random and datetime. You have seen random briefly in Chapter 15. Chapter 24 goes further — covering random choices, simulating repeated trials, seeding for reproducibility, and interpreting results. You will also work with dates and times in Python: creating date objects, formatting them for display, computing durations, and building programs that are aware of when they run. Everything you have learned about functions, loops, data structures, files, and exceptions will apply to the programs you build in Chapter 24.