When something goes wrong in Python — dividing by zero, reading a missing key, parsing bad text — it raises an exception. Left alone, an exception stops the program immediately and prints a traceback: a report of exactly where things broke and what call chain led there.
That is helpful while you are debugging, but it is disastrous in a live program. A user entering a bad number should not crash the whole application. A try/except block lets you catch the exception and respond gracefully:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
Code that might fail goes in try; the recovery plan goes in except. The program survives, and you decide what to do next — return a default, ask again, or log the issue. Without this safety net, every risky operation would need its own guard logic, and your code would drown in checks.
flowchart TD T["try: risky code"] -->|no error| E["else / continue"] T -->|raises| X["except: handle it"] E --> F["finally: cleanup"] X --> F style T fill:#3776ab,color:#fff style X fill:#b91c1c,color:#fff
Catch specific types
Always catch a specific exception type so real bugs still surface. except ValueError catches bad conversions; except ZeroDivisionError catches division by zero. Each type is a signal about what went wrong, and your recovery should match the failure.
You can list several types in one except, and you can chain handlers from most specific to least specific:
try:
value = int('not a number')
except ValueError:
print('that was not a number')
Catching the specific type is also documentation: the next person to read your code knows exactly which failure you expected. A vague handler says "something might go wrong"; a specific one says "I know this input can be invalid, and here is the plan." If you catch everything, you catch bugs you did not know about — and those are the ones you most need to see.
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(safe_divide(10, 2))
print(safe_divide(10, 0))
try:
int('oops')
except ValueError:
print('caught a ValueError')
else and finally
Two optional blocks make try more powerful.
else runs only when the try block succeeds — no exception was raised. It keeps the happy path visible and separate from the error handling. This matters for readability: when else is present, a reader knows the code inside it assumes everything worked.
finally runs no matter what: success, failure, or even an early return. It is the right place for cleanup — closing files, releasing locks, or resetting state.
Together they give you a complete lifecycle: attempt the work, handle the expected failures, do the next step on success, and clean up either way. A function with all four blocks is rare, but knowing they exist lets you choose the right shape for the job.
def process(value):
try:
n = int(value)
except ValueError:
print('bad input')
return
else:
print('parsed:', n)
finally:
print('cleanup done')
process('42')
process('hi')
flowchart TD T["try"] -->|success| E["else"] T -->|error| X["except"] E --> F["finally"] X --> F style T fill:#3776ab,color:#fff style E fill:#16a34a,color:#fff style X fill:#b91c1c,color:#fff style F fill:#1e293b,color:#fff
Raising your own exceptions
You are not limited to the exceptions Python provides. When your function detects a problem it cannot fix, raise an exception and let the caller decide what to do:
def set_age(age):
if age < 0:
raise ValueError('age cannot be negative')
return age
Raising an exception is better than returning a magic value like -1 or None, because those can be silently ignored. An exception forces the caller to handle the problem or admit they are ignoring it. Use built-in types like ValueError and TypeError when they fit; create custom exceptions only when you need a type no built-in covers.
The as keyword lets you capture the exception object and read its message: except ValueError as e gives you e, which is useful for logging or displaying friendly error text.
def set_age(age):
if age < 0:
raise ValueError('age cannot be negative')
return age
try:
print(set_age(25))
print(set_age(-3))
except ValueError as e:
print('caught:', e)
Write safe_int(value) that returns int(value) when it can be parsed, and returns 0 otherwise. Catch ValueError (and TypeError, in case value is not a string or number).
def safe_int(value):
# try int(value); on failure return 0
pass
Wrap int(value) in a try block.
except (ValueError, TypeError): return 0.
def safe_int(value):
try:
return int(value)
except (ValueError, TypeError):
return 0
What does this print? The division by zero is caught, and finally always runs.
try:
result = 10 // 0
print('ok')
except ZeroDivisionError:
print('caught')
finally:
print('done')
10 // 0 raises ZeroDivisionError, so 'ok' is never printed.
The except prints 'caught', then finally prints 'done'.
Write validate_positive(n) that returns n if it is greater than zero. Otherwise raise ValueError with the exact message 'must be positive'.
def validate_positive(n):
# raise ValueError if n <= 0
pass
Use
raise ValueError('must be positive')inside an if block.Return n when it passes the check.
def validate_positive(n):
if n <= 0:
raise ValueError('must be positive')
return n
This function is meant to catch ValueError when parsing fails, but its bare except also hides NameError bugs. Fix it to catch only ValueError.
def parse_number(text):
try:
return int(text)
except:
return 0
Replace
except:withexcept ValueError:.This still returns 0 for bad input, but lets other errors through.
def parse_number(text):
try:
return int(text)
except ValueError:
return 0
Write divide_all(numbers, divisor) that returns a list of n / divisor for each n in numbers. If divisor is zero, return an empty list instead of crashing. Use try/except inside the loop.
def divide_all(numbers, divisor):
# return list of n/divisor, or empty list if divisor is 0
pass
Use a list comprehension for the happy path.
Wrap the comprehension in try/except ZeroDivisionError.
def divide_all(numbers, divisor):
try:
return [n / divisor for n in numbers]
except ZeroDivisionError:
return []
Recap
tryholds code that might fail;exceptcatches a specific exception type and recovers.elseruns only whentrysucceeds;finallyruns no matter what.raiselets you signal problems your code cannot fix, forcing the caller to handle them.- A bare
except:hides real bugs — always name the type you expect. - Specific handlers are documentation: they tell the reader exactly which failure you planned for.
- Use
except ExceptionType as eto capture the error message for logging or user feedback.
Next you'll meet functions, the tool that lets you wrap this error-handling logic into a reusable, testable unit.