Error Handling in Python: try, except and Best Practices
How to handle errors in Python so failures are visible and recoverable: the exact order try, except, else and finally run in, why a broad except hides bugs, designing custom exceptions, chaining with raise from, logging tracebacks, and retrying only what is worth retrying.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
Error handling is not about preventing failures. Files go missing, networks drop and users type "three" into a quantity box. It is about deciding, for each failure, whether your code can do something useful about it, and making sure that everything else fails loudly and clearly. Python gives you four keywords and a hierarchy of exception classes for that. This guide covers how they behave and the habits that separate robust code from code that hides its bugs.
The order try, except, else and finally run in
def load_quantity(raw: str) -> int:
try:
qty = int(raw)
except ValueError:
print(f" except: {raw!r} is not an integer")
return 0
else:
print(f" else: parsed {qty}")
return qty
finally:
print(" finally: always runs")
for raw in ["3", "three"]:
print(raw, "->", load_quantity(raw)) else: parsed 3
finally: always runs
3 -> 3
except: 'three' is not an integer
finally: always runs
three -> 0| Block | Runs when | Put here |
|---|---|---|
try | Always, until an exception is raised | Only the lines that can fail in the way you are handling |
except | A matching exception was raised in try | The recovery for that failure |
else | try finished without an exception | Code that should run only on success |
finally | Always, even after return or an unhandled exception | Clean-up: closing files, releasing locks |
The else block is underused. Code in else runs only on success, but its own exceptions are not caught by the except above it, so you do not accidentally handle an error you never meant to (Python tutorial: errors and exceptions). For clean-up, a with statement usually beats finally: files, locks and database connections close themselves when the block exits.
Catch what you can handle, and nothing more
A broad except turns bugs into silent wrong answers:
def total_with_tax(prices):
try:
return sum(prices) * TAX_RATE # TAX_RATE was never defined
except Exception:
return 0
print(total_with_tax([100, 250]))0The NameError is a programming mistake, and the program reports a total of zero instead of crashing where the bug is. Catch the specific exception you expect, ValueError for bad conversions, FileNotFoundError for a missing file, KeyError for a missing key, and let everything else propagate.
A bare except: is worse still. It catches KeyboardInterrupt and SystemExit, which deliberately sit outside the Exception branch of the hierarchy so that Ctrl+C and sys.exit() keep working (Python docs: built-in exceptions). If you truly need a last-resort handler, say at the top of a worker loop, catch Exception, log it with the traceback, and keep the scope as small as possible.
Custom exceptions that carry meaning
When your own code detects a problem, raise an exception that says what went wrong in your domain's terms. A small hierarchy lets callers catch everything from your module, or just one case:
class OrderError(Exception):
"""Base class for errors this module raises on purpose."""
class OutOfStock(OrderError):
def __init__(self, sku: str, requested: int, available: int):
super().__init__(f"{sku}: requested {requested}, only {available} left")
self.sku, self.requested, self.available = sku, requested, available
STOCK = {"A1": 2}
def reserve(sku: str, qty: int) -> None:
try:
available = STOCK[sku]
except KeyError as exc:
raise OrderError(f"unknown SKU {sku!r}") from exc
if qty > available:
raise OutOfStock(sku, qty, available)
STOCK[sku] = available - qty
for sku, qty in [("A1", 5), ("Z9", 1)]:
try:
reserve(sku, qty)
except OutOfStock as exc:
print("out of stock:", exc, "| can offer", exc.available)
except OrderError as exc:
print("order error:", exc, "| caused by", type(exc.__cause__).__name__)out of stock: A1: requested 5, only 2 left | can offer 2
order error: unknown SKU 'Z9' | caused by KeyErrorThree details make this work:
- Attributes, not just a message.
OutOfStockcarriesavailable, so the caller can offer the two units in stock without parsing a string. - Order of except clauses. Python uses the first clause that matches, so the specific
OutOfStockcomes before its parentOrderError. - raise ... from exc. The internal
KeyErrorbecomes the new exception's__cause__. The traceback shows both, joined by "The above exception was the direct cause of the following exception", so the translation into domain terms loses no debugging information.
Ask forgiveness, not permission
Python style often prefers trying the operation and handling the failure (EAFP) over checking first (LBYL). Checking first has a race: the file can disappear between exists() and open() (Python glossary: EAFP).
from pathlib import Path
try:
text = Path("settings.json").read_text(encoding="utf-8")
except FileNotFoundError:
text = "{}" # no settings file yet: use defaults
print(text){}Log it with the traceback, once
When you catch an exception you cannot fix, but must not crash on, such as one failed record in a batch of 10,000, record it properly. logging.exception() logs at ERROR level and appends the full traceback; call it inside the except block (Python docs: logging):
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("orders")
try:
reserve("A1", 99)
except OrderError:
log.exception("reservation failed for order %s", 1042)ERROR reservation failed for order 1042
Traceback (most recent call last):
File "...", line ..., in <module>
reserve("A1", 99)
File "...", line ..., in reserve
raise OutOfStock(sku, qty, available)
OutOfStock: A1: requested 99, only 2 left(The file paths and line numbers are elided here; yours will show your own.) Log each failure once, at the level that handles it. Logging and re-raising at every layer produces the same traceback five times. A logging configuration for real projects is covered in Python logging: a practical setup.
Retry only what is transient
Some failures go away on their own: a dropped connection, a timeout, a rate limit. Retrying those, with a growing delay, is reasonable. Retrying a ValueError is not; the same input fails the same way every time.
import time
def with_retries(fn, attempts=3, base_delay=0.1, retry_on=(ConnectionError, TimeoutError)):
for attempt in range(1, attempts + 1):
try:
return fn()
except retry_on as exc:
if attempt == attempts:
raise # give up: let the caller see the real error
delay = base_delay * 2 ** (attempt - 1)
print(f"attempt {attempt} failed ({exc}); retrying in {delay:.1f}s")
time.sleep(delay)
outcomes = iter([ConnectionError("reset by peer"), TimeoutError("timed out"), "OK"])
def flaky_call():
result = next(outcomes)
if isinstance(result, Exception):
raise result
return result
print(with_retries(flaky_call))attempt 1 failed (reset by peer); retrying in 0.1s
attempt 2 failed (timed out); retrying in 0.2s
OKA bare raise inside except re-raises the current exception with its original traceback, which is what you want after the last attempt. Only retry operations that are safe to repeat; retrying a payment that may already have gone through needs an idempotency check first.
Batch jobs: collect failures, then decide
Data pipelines process thousands of records, and one malformed row should not stop the other 9,999. Handle errors per record: catch the specific exceptions a bad record can cause, write the record and the error message to a quarantine list or file, and continue. At the end, report the counts, and fail the whole job only if the failure rate crosses a threshold you chose in advance, say 1%. A job that never fails hides data problems; a job that fails on the first bad row cannot finish. Deciding the threshold up front turns an argument during an incident into a setting.
Rules of thumb
- Keep
tryblocks short: only the lines whose failure you are handling. - Catch specific exceptions; never use a bare
except:. - Never write
except ...: passwithout a comment that explains why ignoring the error is correct. - Raise your own exception classes for your own failure cases, and chain with
from. - Prefer raising a clear exception to returning
Noneon failure. A function that returnsNoneforces every caller to check, and the caller that forgets fails far from the cause. - Let unexpected errors crash, loudly, where they happen. A clear traceback is a gift to whoever fixes it.
- Test the failure paths too:
pytest.raiseschecks that code raises what it should; see unit testing with pytest.
Concurrent code adds exception groups and except*, which the asyncio guide demonstrates with TaskGroup. Missing configuration is a classic startup failure; environment variables and secrets shows a loader that fails fast. Exception handling, custom exceptions and logging basics form one module of the Python Programming course.
Sources
- Errors and Exceptions (tutorial) — Python documentation
- Built-in Exceptions — Python documentation
- Glossary: EAFP — Python documentation
- logging — Logger.exception — Python documentation
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.