What you'll learn
Quick Answer
Catch specific exceptions, never a bare except. try holds the risky code, except handles a named failure, else runs only when nothing went wrong, and finally always runs. If you cannot do something useful in the handler, do not catch it.
The basic shape
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "cannot divide by zero"
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # cannot divide by zero
Note that 10 / 2 gives 5.0, not 5 — division always returns a float in Python 3. Use // for integer division.
The important part is except ZeroDivisionError rather than except. You are stating exactly which failure you anticipated and know how to handle. Everything else remains an error, which is what you want.
Why a bare except is a bug factory
# Do not do this
try:
result = compute()
except:
pass
This catches everything — including typos in your own code. If compute is misspelled, you get a NameError, which this swallows silently. The program continues with result undefined and fails somewhere unrelated, minutes later, with a message that points at the wrong place.
Bare except also catches KeyboardInterrupt, so Ctrl+C stops working, which is genuinely infuriating in a long-running script.
The rule: catch what you can handle, and let everything else crash. A crash with a traceback tells you exactly what went wrong and where. Silent failure tells you nothing, and it is much more expensive to debug.
The exceptions you will meet most
Each carries useful information in its message, which is worth reading rather than skimming:
try:
int("abc")
except ValueError as e:
print(e) # invalid literal for int() with base 10: 'abc'
try:
{"a": 1}["b"]
except KeyError as e:
print(e) # 'b'
KeyError prints the key that was missing — often the entire debugging session in one line. Others you will meet constantly: TypeError (wrong type, such as adding a string to a number), IndexError (list index out of range), FileNotFoundError, and AttributeError (calling a method the object does not have, usually because it is None).
else and finally, which most people skip
try:
x = 5
except Exception:
print("failed")
else:
print("else ran, no exception")
finally:
print("finally always runs")
else runs only if no exception occurred. It is useful because it keeps the try block down to the single risky line — if you put the follow-up code inside try as well, an exception raised by that code gets caught by a handler written for something else entirely.
finally runs no matter what, including when the function returns or the exception propagates. It is for cleanup: closing files, releasing locks, closing connections.
In practice, with open(...) replaces most manual finally blocks for files, because the context manager guarantees the file closes even if an error occurs.
Raising your own
Exceptions are not only for the language to throw at you. Raising one is how a function refuses invalid input:
def set_age(age):
if age < 0:
raise ValueError(f"age cannot be negative: {age}")
return age
This is better than returning None or -1 to signal failure, because a returned error code can be ignored by the caller and an exception cannot. Include the offending value in the message — "age cannot be negative: -5" is far more useful than "invalid input".
When you catch an exception only to log it, re-raise it afterwards with a bare raise so the original traceback is preserved. Catching, logging and continuing is how a bug becomes invisible.
