Day 17 · ~16m

Python try except finally: Professional Error Handling

Bare except swallows everything, even bugs. Learn to catch specific exceptions and use finally for cleanup that always runs.

student (struggling)

I've been chasing a bug for a week. A calculation is off by sometimes 30 cents, sometimes way more. Disappears when I add a print statement. I added like... five debugging lines. Finally traced it to a function that's silently accepting strings when it should reject them.

teacher (serious)

You wrapped it in except Exception?

student (thinking)

Yeah. And just did pass. Figured at least it wouldn't crash the whole script.

teacher (neutral)

That except just swallowed the error that was trying to tell you what was wrong. It's not safety. It's hiding the problem from yourself.

student (surprised)

Wait, I thought catching Exception was the right thing to do. Isn't bare except even worse?

teacher (focused)

Much worse. Bare except catches EVERYTHING — including KeyboardInterrupt when someone presses Ctrl+C trying to kill your script. But you're half-right. There are three levels of bad here.

Worst: bare except:

try:
    total = float(value)
except:  # catches KeyboardInterrupt, SystemExit, everything
    total = 0

Bad: except Exception:

try:
    total = float(value)
except Exception:  # catches almost everything, but at least you can kill the script
    total = 0

Good: specific exceptions

try:
    total = float(value)
except ValueError:  # only this error
    total = 0
except TypeError:  # if it was a completely wrong type
    total = 0
student (confused)

Why does it matter if I catch Exception or ValueError? Either way I'm handling the error, right?

teacher (encouraging)

Imagine your script is 50 lines. Line 30 has your try/except. Line 45 has a typo that causes a NameError — you typed ordeers instead of orders. You wrapped it in except Exception three layers up. That NameError gets caught and hidden. You never see it. The bug lives for weeks.

student (thinking)

But if I only catch ValueError, the NameError would crash and tell me the line number.

teacher (serious)

Exactly. You catch what you expect to go wrong. Everything else crashes loudly and tells you where. That's how bugs surface instead of hiding.

student (curious)

Okay so I need to know what exception float() raises when it fails. How do I know that?

teacher (focused)

Test it. Try it, let it fail, read the error:

float("hello")
# ValueError: could not convert string to float: 'hello'

That's a ValueError. If you pass None:

float(None)
# TypeError: float() argument must be a string or a number, not 'NoneType'

That's TypeError. Python tells you exactly what went wrong and what type of error it is.

student (focused)

So I should catch those two specifically.

teacher (neutral)

And raise a more helpful error yourself:

try:
    total = float(value)
except ValueError:
    raise ValueError("total must be a number")
except TypeError:
    raise ValueError("total cannot be None")

Now the error message is specific to your use case, not Python's generic one.

student (thinking)

So I'm converting the low-level Python error into an error that makes sense in my domain.

teacher (proud)

Exactly. A ValueError from float() is "I don't understand the syntax." Your ValueError is "this field is required and must be numeric." Different meaning, different action.

student (excited)

And if something ELSE breaks in that function — something I didn't anticipate — it crashes and tells me.

teacher (encouraging)

Your week-long bug chase ends in five minutes because the crash tells you exactly where to look.

student (curious)

What about finally? You mentioned it.

teacher (neutral)

finally runs no matter what — success, exception caught, exception not caught. It's for cleanup:

file = open("orders.csv")
try:
    data = file.read()
except IOError:
    print("couldn't read file")
finally:
    file.close()  # ALWAYS runs, even if the exception wasn't caught
student (confused)

Why not just close it after the try block?

teacher (serious)

Because if an exception happens, you skip the next line. Watch:

file = open("orders.csv")
try:
    data = file.read()
except IOError:
    print("couldn't read file")
file.close()  # What if IOError happened? You might not get here.

Actually, you would get there — but what if you don't have an except clause?

file = open("orders.csv")
try:
    data = file.read()
    # If this raises something OTHER than IOError, it crashes
    # and file.close() never runs
file.close()  # Skipped if exception happened

That's a resource leak. File stays open, consuming memory and maybe a connection slot. With finally:

file = open("orders.csv")
try:
    data = file.read()
finally:
    file.close()  # Always runs, whether exception happened or not
student (thinking)

So finally is a guarantee. Code that MUST run.

teacher (focused)

Cleanup code. File closes, connection closes, lock releases, state resets. It happens even if the exception wasn't caught and the program is about to crash.

student (curious)

Can I have finally without except?

teacher (neutral)

Absolutely:

try:
    result = risky_operation()
finally:
    cleanup()  # runs even if exception is not caught
student (thinking)

So the exception still propagates up, but cleanup runs first.

teacher (encouraging)

That's the whole purpose. Cleanup is guaranteed, exceptions are honest.

student (amused)

Okay so the hierarchy is: try (the dangerous thing), except (handle specific errors you expect), finally (cleanup that must happen). And if you catch Exception or use bare except, you hide bugs. That's... a lot of good practices from one keyword.

teacher (amused)

Error handling is the entire difference between "code that sometimes works" and "code you can trust."

student (focused)

All right. Let me write a function that parses an order total. If it's bad data, raise ValueError with a specific message. Use finally to... actually, what cleanup would I need here?

teacher (serious)

Good question — not every try needs finally. Finally is for resources: files, database connections, locks. Simple type conversion usually doesn't need cleanup. Use finally when you open something that needs closing, lock something that needs unlocking, or set state that needs resetting.

student (thinking)

So try/except is for "what can go wrong in this operation," and finally is for "what cleanup must always happen if we opened something."

teacher (focused)

If you want to see them together:

connection = connect_to_database()
try:
    orders = connection.query("SELECT * FROM orders")
except ConnectionError:
    orders = []  # fallback to empty list
finally:
    connection.close()  # Always close, whether the query succeeded or failed
student (proud)

That makes sense. Catch what you expect, let the rest crash, and guarantee the cleanup.

teacher (excited)

That's the entire week of error handling. Tomorrow you learn that Python has dozens of built-in exceptions, and the day after you invent your own — custom exceptions for your domain.