When writing Python programs, errors are inevitable. Whether you’re reading a file, parsing user input, or making network requests, things can (and will) go wrong at runtime. If not handled properly, these failures can crash your application and frustrate users.
That’s where exception handling in Python comes in. By mastering try/except, else, finally, and context managers (with), you can write resilient code that handles errors gracefully while still surfacing real bugs when they occur.
In this guide, we’ll walk through the best practices for handling exceptions in Python, complete with code examples, so you can keep your programs reliable, readable, and production-ready.
Core Principles of Exception Handling in Python
Before diving into methods, keep these golden rules in mind:
- Catch only specific exceptions you expect (
ValueError,FileNotFoundError) instead of using blanket handlers. - Use
elsefor success paths andfinallyfor guaranteed cleanup. - Favor context managers (
with) for files and resources. - Never silently swallow bugs — log and re-raise them instead.
- Reserve catch-all handlers for top-level boundaries where you need to log or fail fast.
Method 1 — Catch Specific Exceptions (Recommended)
Instead of handling every possible error, catch the exact ones you expect:
raw = input("Select a fruit number (0-2): ")
try:
selection = ["apple", "pear", "banana"][int(raw)]
print("You chose:", selection)
except ValueError:
print("Not a number.")
except IndexError:
print("Choice out of range.")
You can also group exceptions when recovery is the same:
try:
selection = ["apple", "pear", "banana"][int(raw)]
except (ValueError, IndexError) as e:
print("Invalid selection:", type(e).__name__)
Common exceptions to know:
OSErrorfamily → File handlingValueError/TypeError→ Data issuesZeroDivisionError→ Math errorsKeyError/IndexError→ Collections
Method 2 — Use else and finally for Clean Separation
else runs only if no error occurs, while finally always runs for cleanup.
try:
f = open("config.json", "r", encoding="utf-8")
except FileNotFoundError:
print("No config file found.")
else:
data = f.read()
print("Loaded", len(data), "bytes.")
f.close()
With finally:
f = None
try:
f = open("config.json", "r", encoding="utf-8")
data = f.read()
except OSError as err:
print("OS error:", err)
finally:
if f:
f.close()
Method 3 — Guard the Program Entry Point
Wrap your main workflow with a top-level exception handler to log and fail fast.
import logging, sys
logging.basicConfig(level=logging.INFO)
def main():
# core workflow
return 0
if __name__ == "__main__":
try:
code = main()
except Exception:
logging.exception("Unhandled error")
sys.exit(1)
sys.exit(code)
Returning a non-zero exit code signals automation tools that the run failed.
Method 4 — Use Context Managers (with) for Resources
Context managers automatically clean up files, sockets, and other resources.
from pathlib import Path
try:
with Path("data.txt").open("r", encoding="utf-8") as f:
print(f.readline().strip())
except FileNotFoundError:
print("Create data.txt first.")
Keep only risky operations inside the try block:
try:
with open("image.png", "rb") as fh:
blob = fh.read()
except OSError as e:
print("File access failed:", e)
# process blob outside the block
Method 5 — Raise, Re-Raise, and Chain Exceptions
Raise when conditions fail:
def parse_age(s: str) -> int:
if not s.isdigit():
raise ValueError("Age must contain only digits")
age = int(s)
if age < 0:
raise ValueError("Age cannot be negative")
return age
Re-raise after logging:
import logging
try:
do_risky_thing()
except OSError as err:
logging.error("OS error: %s", err)
raise
Chain exceptions with context:
from pathlib import Path
import json
def load_json(path: str):
try:
text = Path(path).read_text(encoding="utf-8")
except OSError as e:
raise RuntimeError(f"Failed to read {path}") from e
return json.loads(text)
Method 6 — Catch-All Handling (Use Sparingly)
Sometimes you need a broad safety net:
import traceback
try:
risky()
except Exception as e:
print(f"Unexpected {type(e).__name__}: {e}")
traceback.print_exc()
Avoid catching BaseException unless you’re handling shutdown signals:
try:
service_loop()
except BaseException as e:
print(f"Caught {type(e).__name__}; shutting down cleanly.")
raise
Method 7 — Handle Multiple Errors with ExceptionGroup (Python 3.11+)
Aggregate multiple failures with ExceptionGroup:
def run_tests(tests):
errors = []
for t in tests:
try:
t.run()
except Exception as e:
e.add_note(f"Test {t.name} failed")
errors.append(e)
if errors:
raise ExceptionGroup("Batch failures", errors)
Handle them selectively with except*:
try:
run_tests(tests)
except* (ValueError, TypeError):
print("Some data errors occurred.")
except* OSError:
print("Some OS errors occurred.")
Quick Troubleshooting Patterns
- Input parsing → wrap
int()orfloat()withexcept ValueError - File I/O → catch
FileNotFoundError,PermissionError, orOSError - Networking/API calls → handle timeouts, retries, and connection errors
- Always log exceptions → use
logging.exception()ortraceback.print_exc()
Conclusion
Exception handling in Python isn’t just about preventing crashes — it’s about writing clean, maintainable, and fault-tolerant code. By catching specific errors, using else and finally wisely, leveraging context managers, and applying catch-all strategies only when appropriate, you can create applications that are both robust and easy to debug.
Master these techniques, and you’ll be equipped to handle anything Python throws your way — from simple input mistakes to complex multi-error workflows.
Add Tech2Geek as a preferred source on Google to see more of our latest tech articles.
Support Tech2Geek ❤️
Did this guide help you? If Tech2Geek saves you time or helps you solve a problem,
consider buying me a coffee. ☕
Your support helps keep Tech2Geek independent and free.


Comments