Back to Blog
Handling Errors Like a Professional Python Developer

Handling Errors Like a Professional Python Developer

August 20, 2026
Reynov Christian

Python distinguishes between syntax, runtime, and logic errors. Developers use try and except blocks to catch runtime exceptions, allowing programs to recover or fail gracefully. This approach prevents crashes and provides clear feedback to users when unexpected inputs or operations occur during execution.

Advanced techniques include catching specific exception types, using finally for cleanup, and defensive programming to validate data. Proper debugging involves reading tracebacks and using the raise keyword to trigger custom errors. These practices ensure code remains robust, maintainable, and easy for others to understand.

If you have followed this series so far, your programs have probably crashed at least once. That is not a failure, it is a normal part of learning to code. Every programmer, no matter how experienced, deals with errors constantly. What separates a beginner program from a professional one is not the absence of errors, it is how gracefully the program handles them when they happen.

Three Kinds of Errors

Not all errors are the same, and knowing which kind you are looking at saves a lot of confusion.

  • Syntax errors: Python cannot even understand your code, usually because of a typo, a missing colon, or an unclosed bracket. These are caught before the program runs at all.
  • Runtime errors: the code is written correctly, but something goes wrong while it runs, like dividing by zero or trying to open a file that does not exist. These are also called exceptions.
  • Logic errors: the program runs fine and produces no error message, but the result is wrong. These are the hardest to catch, since Python has no way of knowing your intended answer.
print("Missing parenthesis"   # SyntaxError

print(10 / 0)                  # ZeroDivisionError, a runtime error

def add(a, b):
    return a - b                # logic error: this should add, not subtract

This article focuses mainly on runtime errors, since those are the ones you can catch and recover from while your program is running. Syntax errors need to be fixed in your code before the program can run at all. Logic errors are usually caught through testing, the kind of asserts you saw in the previous article on functions.

Try and Except: Catching Errors Gracefully

try:
    age = int(input("Enter your age: "))
    print(f"Next year you'll be {age + 1}")
except ValueError:
    print("That doesn't look like a valid number.")

Python runs the code inside the try block first. If everything works, the except block is skipped entirely. But if an error happens anywhere inside the try block, Python immediately jumps to the matching except block instead of crashing the whole program. In this example, typing letters instead of digits raises a ValueError when int() tries to convert them, and the program prints a friendly message instead of an ugly crash.

Without this try and except block, the same bad input would stop your entire program with a message most users would not understand, something like ValueError: invalid literal for int() with base 10: 'abc'. Catching the error lets your program recover and keep going, or at least fail politely.

Catching Specific Exception Types

numbers = [10, 20, 30]

try:
    index = int(input("Which position (0, 1, or 2)? "))
    print(numbers[index] / 0)
except ValueError:
    print("Please enter a whole number.")
except IndexError:
    print("That position doesn't exist in the list.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

You can stack several except blocks, one for each type of error you expect. Python checks them in order and runs the first one that matches. This matters because each error type usually needs a different response. Telling a user “please enter a whole number” when they actually typed a valid number that was simply out of range would be confusing and unhelpful.

It is tempting to write one giant except: with nothing after it, catching every possible error the same way. Avoid this. A bare except: also silently swallows real bugs and typos in your own code, like a misspelled variable name, making them much harder to notice and fix. Catch specific exception types you actually expect, and let unexpected ones surface so you can see and fix them.

Accessing the Error Message

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Something went wrong: {e}")

Adding as e after the exception type stores the actual error object in a variable, letting you inspect or print its message. This is useful for logging what went wrong, or for showing more specific feedback than a generic message. Here, e would contain the text division by zero.

Else and Finally

try:
    number = int(input("Enter a number: "))
except ValueError:
    print("Invalid input.")
else:
    print(f"You entered {number}, great job.")
finally:
    print("Input attempt finished.")

Two more blocks can extend a try statement. else runs only if the try block succeeded with no errors at all, which keeps success logic clearly separate from the risky code that might fail. finally runs every single time, whether an error happened or not, whether it was caught or not. This makes finally the right place for cleanup work, like closing a file or a network connection, that absolutely must happen no matter what else went wrong.

Debugging Strategies

When your program does crash, the traceback Python prints is not there to scare you, it is a map. Read it from the bottom up. The last line names the exact error type and message. The lines above it show the chain of function calls that led there, with the most recent call, the one closest to where the error actually happened, at the very bottom of that chain.

  • Read the last line first. It usually tells you exactly what kind of error occurred and often why.
  • Find the line number. The traceback points to the specific line and file where the error happened, so go look at that line directly.
  • Print values as you go. Adding temporary print() statements before a suspicious line, to show what a variable actually contains, is a simple and effective way to narrow down a bug.
  • Change one thing at a time. When testing a fix, resist the urge to change several lines at once. If the bug disappears, you want to know exactly which change fixed it.

Error Prevention: Writing Defensive Code

Catching errors after they happen is useful, but preventing them in the first place is even better. A few habits go a long way. Validate user input before you use it, rather than assuming it is already correct. Check that a list or dictionary actually has the key or index you are about to access. When a function receives a value it cannot work with, consider raising a clear error yourself rather than letting the program fail confusingly somewhere else:

def calculate_discount(price, percent):
    if percent < 0 or percent > 100:
        raise ValueError("Discount percent must be between 0 and 100.")
    return price - (price * percent / 100)

The raise keyword lets you trigger an exception on purpose, with a message that explains exactly what went wrong and why. This is far more helpful to whoever calls this function later, maybe even you in a few weeks, than letting a confusing calculation error appear somewhere else in the program.

Practical Project: Robust Input Validation

def get_valid_age():
    while True:
        try:
            age = int(input("Enter your age: "))
            if age < 0 or age > 120:
                print("Please enter a realistic age between 0 and 120.")
                continue
            return age
        except ValueError:
            print("That's not a valid number, please try again.")


def get_valid_email():
    while True:
        email = input("Enter your email: ").strip()
        if "@" in email and "." in email.split("@")[-1]:
            return email
        print("That doesn't look like a valid email, please try again.")


name = input("What's your name? ")
age = get_valid_age()
email = get_valid_email()

print(f"nThanks, {name}! We've saved your age ({age}) and email ({email}).")

This project combines error handling with the while True loop pattern from the loops article, and reuses the email check idea from the functions article. A few details are worth pointing out. get_valid_age() uses try and except ValueError to catch non-numeric input, but it also uses a plain if check for the realistic range, since a number like 200 converts just fine and never raises an exception on its own. Not every invalid input is an error Python will catch for you, some need your own validation logic on top.

Both functions use the same overall shape: loop forever, try to get valid input, and only return once that input actually passes every check. This means neither function can accidentally return a bad value, since the only way out of the loop is a value that already passed validation. This pattern, sometimes called an input loop, is extremely common in real programs that talk to users directly.

Writing User-Friendly Error Messages

A raw Python traceback is useful for you as the developer, but it means nothing to most users. Compare ValueError: invalid literal for int() with base 10: 'abc' to a message like “Please enter a number, letters aren't allowed here.” The second version tells the user exactly what went wrong and what to do about it, without exposing internal details they cannot act on. Whenever you catch an error that a real user might trigger, take the extra moment to translate it into plain language.

Common Beginner Mistakes

  • Using a bare except: catching every possible error the same way hides real bugs in your own code. Catch specific exception types instead.
  • Catching errors too broadly, too early: wrapping your entire program in one giant try block makes it hard to know which line actually failed. Keep try blocks focused on the specific risky operation.
  • Forgetting that some invalid input does not raise an exception: a number that is technically valid but unrealistic, like an age of 200, needs its own if check, not just a try and except.
  • Ignoring the traceback: the error message and line number Python gives you are usually the fastest way to find the bug. Read them before guessing.

Try It Yourself

Practice these three short exercises in a new file called practice_errors.py before moving on:

  1. Write a function that asks the user for two numbers and divides the first by the second, catching both ValueError and ZeroDivisionError with separate, clear messages.
  2. Add a get_valid_phone() function to the input validation project above, that keeps asking until the user enters a string of exactly 10 to 13 digits.
  3. Deliberately trigger a KeyError by accessing a missing key in a dictionary, without a try and except first. Read the traceback, then wrap it in a try and except that prints a friendly message instead.

Frequently Asked Questions

What's the difference between an error and an exception?

In everyday conversation the two words are often used the same way. In Python specifically, an exception is a runtime error, one that Python represents as an object you can catch with try and except. Syntax errors are not exceptions, since they stop the program before it even starts running.

Should I use try and except around my entire program?

No. Wrap only the specific lines that might realistically fail, like user input or file access. Wrapping everything makes it much harder to know which line actually caused a problem.

What does “raise” actually do?

raise creates and triggers an exception on purpose. It is how you tell the rest of your program, or whoever calls your function, that something is wrong and cannot continue normally.

Is it bad practice to catch every exception with a bare except?

Yes, generally avoid it. A bare except: also catches typos and bugs in your own code, hiding them instead of surfacing them. Catch the specific exception types you actually expect.

When does the finally block run?

Always, no matter what. Whether the try block succeeded, failed, or was even caught by an except block, the code inside finally still runs before the program moves on.

Ready to make your programs remember information between runs? In our next article, we'll explore file handling in Python: reading and writing text files, working with CSV data, and managing file paths safely. You'll build a personal expense tracker that saves your data to a file, so nothing gets lost when you close the program.

Learning programming works best as a collaborative process, and your questions and insights help make these tutorials more effective for everyone. If you get stuck choosing the right exception type, run into an error this article didn't cover, or find a creative way to extend the input validation project, please share your experience in the comments. Your feedback helps create a learning community where we can all benefit from each other's discoveries and solutions.

You Might Also Like

Comments

No comments yet — be the first to start the conversation!

Leave a Comment

0/5000