Why Errors Happen (and Why You Should Plan for Them)
When you run a Python program, you are asking it to follow instructions exactly. But real programs interact with messy reality: users type unexpected input, network connections drop, files might be missing, and data may not be in the format you assumed. When something goes wrong, Python raises an exception. If you do nothing, the program stops and prints an error message (a traceback). That is useful while developing, but it is usually not the experience you want for a small useful script.
Handling errors is about two complementary habits:
- Try/except: catch exceptions that may happen and respond in a controlled way.
- Defensive checks: prevent errors by verifying assumptions before you do something risky.
Good beginner scripts often use both: defensive checks for predictable problems, and try/except for unpredictable or external problems.
Understanding Exceptions in Plain Terms
An exception is an object that represents an error condition. Python raises it when it cannot continue normally. Common examples you will see in beginner scripts include:
- ValueError: a value is the right type but has an invalid content (for example, converting "abc" to an integer).
- TypeError: you used the wrong type (for example, adding a number to a string).
- KeyError: a dictionary key was not found.
- IndexError: a list index was out of range.
- ZeroDivisionError: division by zero.
When an exception is raised and not handled, Python stops the program at that point. Handling exceptions lets your program choose what to do instead: ask again, use a default value, skip a bad record, or show a friendly message.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
The Basic Try/Except Pattern
The simplest structure is:
try: # code that might fail risky_operation()except SomeError: # what to do if that specific error happens handle_it()Python runs the code inside try. If it raises SomeError, Python jumps to the except block. If no error happens, the except block is skipped.
Example: Converting User Input to an Integer
A classic error is converting text to a number. If the user types something that is not a valid integer, int() raises ValueError.
raw = input("Enter your age: ")try: age = int(raw)except ValueError: print("Please enter a whole number.") age = 0print("Age stored as:", age)This keeps the program from crashing. However, setting age = 0 might not be the best behavior. Often you want to ask again until you get a valid value.
Practical Step-by-Step: A Safe Number Prompt
One of the most useful patterns for beginner scripts is a “safe prompt” that keeps asking until it gets valid input. Here is a step-by-step approach.
Step 1: Identify the risky operation
The risky operation is converting input text to a number:
number = int(input("Enter a number: "))This can raise ValueError.
Step 2: Wrap it in try/except
try: number = int(input("Enter a number: "))except ValueError: print("That was not a valid integer.")Now it won’t crash, but it still doesn’t produce a number if invalid input happens.
Step 3: Repeat until valid
while True: raw = input("Enter a number: ") try: number = int(raw) break except ValueError: print("That was not a valid integer. Try again.")print("You entered:", number)This pattern is robust and friendly. The loop continues only when conversion succeeds.
Step 4: Add extra defensive checks (range, rules)
Sometimes a value can be a valid integer but still not acceptable (for example, a menu choice must be 1–5). That is not a conversion error; it is a rule check. Use an if check after conversion.
while True: raw = input("Choose an option (1-5): ") try: choice = int(raw) except ValueError: print("Please enter a whole number.") continue if 1 <= choice <= 5: break print("Choice must be between 1 and 5.")print("You chose:", choice)Notice how try/except handles the conversion problem, and the defensive check handles the business rule.
Catching Specific Exceptions (Not Everything)
It is tempting to write:
try: do_something()except Exception: print("Something went wrong")This catches almost all errors, including ones you did not expect. That can hide bugs and make debugging harder. A better beginner habit is:
- Catch the specific exceptions you expect.
- Keep the
tryblock as small as possible (only the lines that might raise the exception you are handling).
Example: Dictionary Lookup with a Fallback
If you access a missing key with dict[key], Python raises KeyError. You can handle it, but often a defensive approach is cleaner.
Try/except approach:
prices = {"apple": 0.50, "banana": 0.30}item = input("Item: ")try: price = prices[item]except KeyError: print("Unknown item") price = Noneprint("Price:", price)Defensive approach using a safe lookup:
prices = {"apple": 0.50, "banana": 0.30}item = input("Item: ")price = prices.get(item)if price is None: print("Unknown item")else: print("Price:", price)In many cases, get() is preferable because it avoids exceptions entirely for a common situation.
Else and Finally: Two Helpful Add-ons
Using else with try/except
The else block runs only if no exception happened in the try block. This can make your code clearer by separating the “success path” from the “error path.”
raw = input("Enter a percentage (0-100): ")try: pct = int(raw)except ValueError: print("Not a whole number")else: if 0 <= pct <= 100: print("Stored:", pct) else: print("Out of range")Here, conversion errors are handled in except, and the range check happens only when conversion succeeded.
Using finally for cleanup
The finally block runs no matter what: whether an exception happened or not. It is mainly used for cleanup actions that must happen even if something fails (for example, releasing a resource, stopping a timer, resetting a state variable).
print("Starting task")try: x = 10 / 0except ZeroDivisionError: print("Math error")finally: print("Task finished (cleanup done)")Even though division fails, the final message still prints.
Defensive Checks: Preventing Errors Before They Happen
Defensive programming means you assume inputs and data can be wrong, and you check conditions before performing operations that could fail. This is not about being pessimistic; it is about building scripts that behave predictably.
Common Defensive Checks You Can Use
- Check for empty input: user pressed Enter without typing.
- Check numeric rules: range limits, non-negative, not zero.
- Check membership: is a key in a dictionary, is an item in a list of allowed values.
- Check string format: basic patterns like “contains @” for a simple email check (not perfect, but useful).
- Check length: password length, code length, etc.
Example: Avoiding ZeroDivisionError with a Check
raw = input("Enter a divisor: ")try: divisor = float(raw)except ValueError: print("Not a number")else: if divisor == 0: print("Divisor cannot be zero") else: result = 100 / divisor print("Result:", result)The defensive check (divisor == 0) prevents a predictable error.
Choosing Between Try/Except and Defensive Checks
A useful rule of thumb:
- Use defensive checks when you can easily test the condition before doing the operation (for example, check for zero, check membership, check length).
- Use try/except when the failure depends on something external or hard to predict (for example, parsing, conversions, operations that can fail for many reasons).
Also, some operations are naturally “ask forgiveness” in Python: you try the operation and handle failure. Others are naturally “look before you leap”: you check first. Both are acceptable; clarity matters most.
Handling Multiple Exceptions
Sometimes different errors can happen in the same area, and you want different messages or actions.
Separate except blocks
raw = input("Enter a whole number: ")try: n = int(raw) value = 100 / nexcept ValueError: print("That was not a whole number")except ZeroDivisionError: print("Number cannot be zero")else: print("Result:", value)This handles two different problems cleanly.
One except for several exception types
If you want the same handling for multiple exceptions, you can group them:
raw = input("Enter a whole number: ")try: n = int(raw) value = 100 / nexcept (ValueError, ZeroDivisionError): print("Please enter a non-zero whole number")else: print("Result:", value)Capturing the Exception Message
Sometimes you want to show a more detailed message (especially while you are still testing your script). You can capture the exception object with as.
raw = input("Enter an integer: ")try: n = int(raw)except ValueError as e: print("Conversion failed:", e)Be careful about showing raw error messages to end users in a polished script, but it is very helpful while learning and debugging.
Raising Your Own Errors for Invalid States
Not all errors come from Python automatically. Sometimes your program reaches a situation that should never happen if the data is correct. In those cases, you can raise an exception yourself. This is useful when you want to stop the current operation and clearly signal that something is wrong.
def calculate_discount(price, pct): if price < 0: raise ValueError("price cannot be negative") if not (0 <= pct <= 100): raise ValueError("pct must be between 0 and 100") return price * (pct / 100)Here, defensive checks are used, but instead of printing, the function raises a clear error. The caller can decide how to handle it.
Handling a raised error
raw_price = input("Price: ")raw_pct = input("Discount %: ")try: price = float(raw_price) pct = float(raw_pct) amount = calculate_discount(price, pct)except ValueError as e: print("Cannot calculate discount:", e)else: print("Discount amount:", amount)This separates responsibilities: the function enforces rules, and the caller decides the user-facing behavior.
Defensive Checks for Data Structures
Many beginner scripts manipulate lists and dictionaries. Errors often happen when you assume something exists when it does not.
Example: Safe access to a list element
Accessing an index that does not exist raises IndexError. If the index comes from user input or variable data, you can defend against it.
items = ["red", "green", "blue"]raw = input("Pick an index (0-2): ")try: idx = int(raw)except ValueError: print("Not an integer")else: if 0 <= idx < len(items): print("You picked:", items[idx]) else: print("Index out of range")Here, the defensive check uses len(items) so it stays correct even if the list changes size.
Example: Avoiding KeyError with membership checks
scores = {"Ana": 10, "Ben": 7}name = input("Name: ")if name in scores: print("Score:", scores[name])else: print("No score for that name")This is a simple defensive check that avoids exceptions entirely.
Designing Friendly Error Messages
Error handling is not only technical; it is also communication. A good message tells the user:
- What went wrong (in simple terms)
- What input is expected
- What they can do next (try again, choose from options)
Compare these two messages:
- Bad:
ValueError - Better:
Please enter a whole number like 1, 2, or 3.
When you catch an exception, you can provide context without exposing internal details.
Keeping Try Blocks Small and Focused
A common beginner mistake is wrapping too much code in a single try/except. That can make it unclear which line caused the error and can accidentally hide unrelated bugs.
Less clear:
try: raw = input("Number: ") n = int(raw) result = 100 / n print("Result:", result)except ValueError: print("Not a number")except ZeroDivisionError: print("Cannot be zero")Clearer and more controlled:
raw = input("Number: ")try: n = int(raw)except ValueError: print("Not a whole number")else: if n == 0: print("Cannot be zero") else: result = 100 / n print("Result:", result)In the second version, the conversion is the only thing inside the try block, and the rest is handled with normal logic.
When to Let Errors Crash the Program
Not every error should be caught. If an error indicates a programming mistake (for example, using the wrong variable name, passing the wrong type inside your own code), it is often better to let it crash during development so you can fix it. Try/except is most valuable for:
- Unreliable input (user typing, data from outside)
- Operations that can fail due to environment (permissions, missing resources, unavailable services)
- Parsing and conversions
As you build scripts, aim for a balance: catch expected problems and handle them gracefully, but do not hide unexpected bugs.
Mini-Recipe: Validating a Simple Comma-Separated Input
Many small scripts ask for a list of values in one line, like "3, 10, 25". Problems include extra spaces, empty items, or non-numbers. You can combine defensive checks and try/except.
raw = input("Enter comma-separated integers (e.g., 3,10,25): ")parts = [p.strip() for p in raw.split(",")]if any(p == "" for p in parts): print("Please do not leave empty values between commas.")else: numbers = [] bad = False for p in parts: try: numbers.append(int(p)) except ValueError: print("Not an integer:", p) bad = True break if not bad: print("Numbers:", numbers) print("Count:", len(numbers)) print("Max:", max(numbers))What this demonstrates:
- Defensive check for empty segments (like
"1,,2"). - Try/except for conversion of each part.
- Stop early when a bad value is found, instead of continuing with partial results.