try, except, else and finally
try runs risky code, except handles a failure, else runs when nothing failed, and finally always runs. Each block has one job and using the wrong one hides bugs.
- The basic form
- Catching the right thing
- Never write a bare except
- Catching several exceptions
- Inspecting the exception object
- else
- finally
- A return in finally swallows exceptions
- with is usually better than finally
- The full order of execution
- Ask forgiveness, or ask permission
- Nesting and re-raising
- Common mistakes
- Best practices
- Practice
- Conclusion
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
The basic form
try:
value = int(input("Enter a number: "))
print("You entered", value)
except ValueError:
print("That was not a whole number.")Python attempts the try block. If an exception of the named type occurs, the except block runs and the program continues. If no exception occurs, the except block is skipped entirely.
Catching the right thing
data = {"a": 1}
# Too broad - hides every mistake, including typos
try:
print(data["b"])
except Exception:
print("something went wrong")
# Right - names exactly what can fail and why
try:
print(data["b"])
except KeyError:
print("that key is not present")Catch the narrowest exception that describes the failure you expect. A broad except Exception will happily swallow a misspelled variable name and report it as "something went wrong", turning a two second fix into an afternoon.Never write a bare except
# Do not do this
try:
risky()
except: # catches EVERYTHING, including KeyboardInterrupt
passA bare except: catches KeyboardInterrupt and SystemExit as well, so the program cannot be stopped with Ctrl+C and cannot exit cleanly. If you genuinely must catch broadly, write except Exception:, which excludes those two.
Catching several exceptions
def divide(text_a, text_b):
try:
return int(text_a) / int(text_b)
except ValueError:
return "both values must be whole numbers"
except ZeroDivisionError:
return "cannot divide by zero"
print(divide("10", "2")) # 5.0
print(divide("10", "x")) # both values must be whole numbers
print(divide("10", "0")) # cannot divide by zero# One handler for several types
try:
risky()
except (ValueError, TypeError, KeyError):
print("bad input of some kind")Handlers are checked in order and only the first match runs. Order them from the most specific to the most general, or the general one will shadow the rest.
Inspecting the exception object
try:
int("abc")
except ValueError as error:
print(type(error).__name__) # ValueError
print(error) # invalid literal for int() with base 10: 'abc'
print(error.args) # a tuple of the arguments it was raised withtry:
open("missing.txt")
except FileNotFoundError as error:
print(error.filename) # missing.txt
print(error.errno) # 2
print(error.strerror) # No such file or directoryelse
try:
value = int("42")
except ValueError:
print("conversion failed")
else:
print("conversion succeeded:", value) # only when nothing was raised
finally:
print("always runs")Why not put the success code inside try? Because the try block should contain only the line that might fail. Anything else in there is protected by the handler as well, which is exactly the accidental over-catching to avoid:
records = {"count": "abc"}
# Wrong: the handler now also catches failures from process()
try:
count = int(records["count"])
process(count) # if THIS raises ValueError, it is misreported
except ValueError:
print("bad count")
# Right: try holds only the risky conversion
try:
count = int(records["count"])
except ValueError:
print("bad count")
else:
process(count)finally
def read_first_line(path):
handle = None
try:
handle = open(path)
return handle.readline()
except FileNotFoundError:
return None
finally:
if handle:
handle.close()
print("file closed")finally runs no matter what: after a successful try, after a handled exception, after an unhandled one on its way up, and even after a return. It exists for cleanup - closing files, releasing locks, restoring state.
def demo():
try:
return "from try"
finally:
print("finally still runs")
print(demo())finally still runs
from tryA return in finally swallows exceptions
def bad():
try:
raise ValueError("important")
finally:
return "hides the error" # the exception disappears
print(bad()) # hides the error - the ValueError is goneNever return, break or continue from a finally block. It discards any exception in flight, and the failure vanishes without trace.
with is usually better than finally
# Manual cleanup
handle = open("data.txt")
try:
content = handle.read()
finally:
handle.close()
# The same guarantee, with the cleanup built in
with open("data.txt") as handle:
content = handle.read()A context manager closes the file however the block exits. Use with for anything that must be released; keep finally for cleanup that has no context manager of its own.
The full order of execution
def demo(value):
print("--- ", value)
try:
print("try")
result = 10 / value
except ZeroDivisionError:
print("except")
return "handled"
else:
print("else")
return result
finally:
print("finally")
print(demo(2))
print(demo(0))--- 2
try
else
finally
5.0
--- 0
try
except
finally
handledAsk forgiveness, or ask permission
config = {"retries": 3}
# Look before you leap
if "timeout" in config:
timeout = config["timeout"]
else:
timeout = 30
# Easier to ask forgiveness
try:
timeout = config["timeout"]
except KeyError:
timeout = 30
# Best here: the method exists for exactly this
timeout = config.get("timeout", 30)Python leans towards trying the operation and handling the failure, because a check followed by an action can go stale between the two. Use a check when failure is likely and cheap to test for; use try when failure is genuinely exceptional.
Nesting and re-raising
def load(path):
try:
with open(path) as handle:
return handle.read()
except FileNotFoundError:
print(f"warning: {path} is missing, using defaults")
return ""
except PermissionError:
print(f"cannot read {path}")
raise # log it, then let the caller deal with itA bare raise inside a handler re-raises the current exception with its original traceback intact. Use it when you want to observe a failure without taking responsibility for it.
Common mistakes
- Writing a bare
except:. - Catching
Exceptionwhen a specific type was known. - Putting more than the risky line inside
try. - Using
except ... : passwith no comment, silently discarding failures. - Returning from
finallyand losing the exception. - Using exceptions for ordinary control flow where an
ifwould be clearer.
Best practices
- Keep
tryblocks as short as possible and move success code intoelse. - Catch specific exception types and name them with
aswhen the details matter. - Use
withfor resources; usefinallyonly when there is no context manager. - Log or report before swallowing an error, and never swallow one silently.
- Re-raise with a bare
raisewhen the caller should still know.
Practice
- Write an input loop that keeps asking until a valid positive integer is entered.
- Rewrite a function that uses
tryaround five lines so that only the risky line is inside it. - Demonstrate that
finallyruns after areturn, and then that areturninfinallyhides an exception. - Write a file reader that reports a missing file, re-raises a permission error, and always reports that it finished.
- Show the same lookup handled three ways: with
in, withtry, and withget. Say which you prefer and why.
Conclusion
Put only the risky line in try, name the exception you expect, put the success path in else and the cleanup in finally - or better, in a with block. Never write a bare except, and never return from finally.