March 26, 20265 min read

Python Error Handling: Patterns That Actually Work in Production

try/except/else/finally, custom exceptions, when to catch vs when to crash, bare except anti-patterns, and EAFP vs LBYL in real Python code.

python error-handling best-practices
Ad 336x280

Most Python tutorials teach you try/except and move on. But error handling in real applications is more nuanced than wrapping everything in a try block and printing the error message. Here's how it actually works in production Python code.

The Full try/except/else/finally Block

Most people know try and except. Fewer use else and finally correctly.

try:
    data = json.loads(raw_input)
except json.JSONDecodeError as e:
    logger.error(f"Invalid JSON: {e}")
    return None
else:
    # Only runs if NO exception occurred
    process(data)
finally:
    # Always runs, exception or not
    cleanup()

The else block is underused. Code in else only executes when the try block succeeds without exceptions. Why not just put it in the try block? Because you want the except to only catch exceptions from the specific operation you're guarding, not from your processing logic. Keeping process(data) in the else block means a bug inside process() won't get silently swallowed by the except.

finally runs no matter what — success, exception, even if you return from inside the try. Use it for cleanup: closing files, releasing locks, disconnecting from databases.

The Bare except Anti-Pattern

# Never do this
try:
    do_something()
except:
    pass

This catches everything — including KeyboardInterrupt (Ctrl+C), SystemExit, and MemoryError. You've made your program nearly impossible to stop and completely silent about failures.

At minimum, catch Exception:

try:
    do_something()
except Exception as e:
    logger.error(f"Unexpected error: {e}")
Exception is the base class for all "normal" exceptions but not system-exit events. Even better: catch the specific exceptions you expect and handle them individually.

When to Catch vs When to Let It Crash

This is the most important judgment call in error handling. New developers tend to catch everything. Experienced developers catch less.

Catch when:
  • You can meaningfully recover (retry the request, use a default value, try an alternative path)
  • You need to translate the error for the caller (convert a database error into an HTTP 500)
  • You need to clean up resources
Let it crash when:
  • The error indicates a programming bug (wrong type, missing attribute, index out of range)
  • You have no recovery strategy — catching it just hides the problem
  • You're in development and want the full traceback
A function three layers deep shouldn't be catching and logging generic exceptions. Let the error propagate up to wherever you have enough context to handle it properly.

Custom Exceptions

For any non-trivial application, define your own exception hierarchy:

class AppError(Exception):
    """Base exception for the application."""
    pass

class ValidationError(AppError):
"""Raised when input validation fails."""
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
"""Raised when a requested resource doesn't exist."""
def __init__(self, resource: str, identifier: str):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} '{identifier}' not found")

Custom exceptions let callers catch specific failure modes and respond appropriately. An API handler can catch ValidationError and return a 400, catch NotFoundError and return a 404, and let everything else bubble up as a 500.

Don't create a custom exception for every possible thing that can go wrong. Only create them when the caller needs to distinguish between different failure types.

EAFP vs LBYL

Two philosophies for dealing with potential errors:

LBYL — Look Before You Leap:
if key in dictionary:
    value = dictionary[key]
else:
    value = default
EAFP — Easier to Ask Forgiveness than Permission:
try:
    value = dictionary[key]
except KeyError:
    value = default

Python culturally favors EAFP. The language is designed for it — exceptions are cheap compared to most languages, and the EAFP style avoids race conditions (the thing might exist when you check but be gone by the time you access it).

That said, use common sense. dict.get(key, default) is better than both patterns above. And if the "exceptional" case happens 90% of the time, the EAFP pattern just adds overhead. Use EAFP for genuinely exceptional situations, not for routine control flow.

Logging Errors Properly

print(e) is not logging. Here's what production error logging looks like:
import logging

logger = logging.getLogger(__name__)

try:
result = external_api.fetch(url)
except requests.RequestException as e:
logger.exception("Failed to fetch from API")
# logger.exception automatically includes the full traceback
raise

Key points:


  • Use logger.exception() inside except blocks — it captures the traceback automatically

  • Use logger.error() when you're not in an except block

  • Include context: what were you trying to do? What were the inputs?

  • Re-raise after logging if the caller needs to know about the failure


Don't log and swallow. Either handle the error or re-raise it. Logging and then silently continuing is how you get bugs that take weeks to track down.

The re-raise Pattern

try:
    save_to_database(record)
except DatabaseError:
    logger.exception("Failed to save record %s", record.id)
    raise  # bare raise preserves the original traceback
raise without arguments re-raises the current exception with its original traceback intact. This is critical — raise e would reset the traceback to this line, losing the information about where the error actually originated.

Putting It Together

Good error handling isn't about catching more exceptions. It's about catching the right ones, in the right place, and doing something useful when you catch them. Code that handles errors well is code that fails loudly when something unexpected happens and recovers gracefully from expected failures.

If you want to practice writing Python that handles edge cases properly, CodeUp has challenges that specifically test your ability to write robust code — not just code that works for the happy path.

Ad 728x90