Python Exception Handling: try, except, else, and finally in Real Code
Every program encounters unexpected situations: a file that doesn’t exist, user input that isn’t a number, a network request that times out. Python’s exception handling mechanism gives you a structured way to respond to these situations rather than letting the program crash.
What Are Exceptions?
An exception is an object that Python creates when something goes wrong during execution. If you do not handle it, Python prints a traceback and terminates your program.
# Unhandled exceptionresult = 10 / 0# ZeroDivisionError: division by zeroWrapping risky code in a try block lets you catch and handle the exception instead.
The try/except Block
try: number = int(input("Enter a number: ")) result = 100 / number print(f"Result: {result}")except ZeroDivisionError: print("Cannot divide by zero — please enter a non-zero number")except ValueError: print("That's not a valid integer")Python tries the try block. If a ZeroDivisionError occurs, it runs the first except block. If a ValueError occurs, it runs the second. If neither occurs, both except blocks are skipped. Execution then continues normally after the whole try/except structure.
Always catch specific exceptions. A bare except: with no exception type catches everything, including KeyboardInterrupt (Ctrl+C) and SystemExit, which makes your program harder to stop.
# Too broad — catches things you should not suppresstry: do_something()except: pass
# Better — catch what you actually expecttry: do_something()except ValueError as e: print(f"Value error: {e}")The as Clause: Accessing the Exception
Use as to bind the exception object to a name. The exception carries a message and sometimes additional attributes.
import json
def load_config(path): try: with open(path) as f: return json.load(f) except FileNotFoundError as e: print(f"Config file not found: {e.filename}") return {} except json.JSONDecodeError as e: print(f"Invalid JSON at line {e.lineno}: {e.msg}") return {}
config = load_config("settings.json")The e.filename, e.lineno, and e.msg attributes come from the specific exception types, giving you enough context to show a useful error message.
The else Block: Code That Runs Only on Success
The else block runs if the try block completed without raising any exception. It is the right place for code that should only run when the try block succeeded.
def read_number_from_file(path): try: with open(path) as f: content = f.read().strip() except FileNotFoundError: print(f"File not found: {path}") return None else: # Only runs if open() succeeded try: return int(content) except ValueError: print(f"File content is not an integer: {content!r}") return NoneUsing else keeps the success path separate from error handling, making both easier to read.
The finally Block: Cleanup That Always Runs
finally runs regardless of whether an exception occurred, was caught, or was re-raised. Use it for cleanup that must happen in every case.
def process_file(path): f = None try: f = open(path) data = f.read() return data.upper() except FileNotFoundError: print(f"Cannot find {path}") return None finally: if f: f.close() # always closes the file if it was opened print("File closed")In modern Python, you rarely write finally for file handling because with handles cleanup automatically. But finally is valuable for resources that don’t support the context manager protocol — database connections, network sockets, locks.
The Complete Pattern
All four clauses can appear together, though you will not need all of them in every situation:
def safe_divide(a, b): try: result = a / b # might raise ZeroDivisionError except ZeroDivisionError: print("Cannot divide by zero") result = None else: print(f"{a} / {b} = {result}") # runs only on success finally: print("Division attempt complete") # always runs return result
safe_divide(10, 2)# 10 / 2 = 5.0# Division attempt complete
safe_divide(10, 0)# Cannot divide by zero# Division attempt completeNested try Blocks
Sometimes you need different handling for different parts of a function:
def fetch_and_parse(url): import urllib.request import json
try: response = urllib.request.urlopen(url, timeout=5) except urllib.error.URLError as e: raise ConnectionError(f"Failed to reach {url}: {e.reason}") from e
try: data = json.loads(response.read().decode()) except json.JSONDecodeError as e: raise ValueError(f"Server returned invalid JSON: {e}") from e
return dataSeparating network errors from parsing errors makes the failure modes explicit to callers.
Common Mistakes
Silently swallowing exceptions. An empty except block hides bugs and makes debugging nearly impossible.
# Anti-pattern — you'll never know what went wrongtry: complex_operation()except Exception: pass # silent failureCatching Exception too broadly. Unless you are at the very top of your call stack (like a global error handler), catching all Exceptions masks specific problems you should be handling individually.
Using exceptions for flow control. Exceptions are for exceptional situations. Using try/except to replace if checks is a code smell in most cases:
# Avoid: using exceptions as flow controltry: value = my_dict["key"]except KeyError: value = "default"
# Better: use dict.get()value = my_dict.get("key", "default")Forgetting that finally runs even if you return from the try block. The finally block executes before the return actually happens, which can cause surprising behaviour if you modify the return value inside finally.