Python
try-except-else
error handling
programming best practices
Python exceptions

Is it a good practice to use try-except-else in Python?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Yes, try-except-else is a good practice in Python and is explicitly recommended in the official Python documentation and PEP 8. The else block runs only when the try block completes without raising an exception, which narrows the scope of the except clause to just the code that might actually fail. This prevents accidentally catching exceptions from code that should not be guarded, making error handling more precise and bugs easier to diagnose.

The Structure of try-except-else-finally

python
1try:
2    # Code that might raise an exception
3    result = some_risky_operation()
4except SomeException as e:
5    # Handle the exception
6    print(f"Operation failed: {e}")
7else:
8    # Runs ONLY if no exception was raised in try
9    process(result)
10finally:
11    # Always runs, regardless of exception
12    cleanup()

The execution flow is: tryexcept (if exception) OR else (if no exception) → finally (always). The else block is skipped entirely if an exception occurs.

Why else Makes Error Handling More Precise

python
1# WITHOUT else — exception scope is too broad
2try:
3    data = json.loads(raw_text)       # Can raise JSONDecodeError
4    save_to_database(data)            # Can raise DatabaseError
5except json.JSONDecodeError:
6    print("Invalid JSON")
7# Problem: if save_to_database raises JSONDecodeError
8# (unlikely but possible), it gets caught here too!
9
10# WITH else — only json.loads is guarded
11try:
12    data = json.loads(raw_text)
13except json.JSONDecodeError:
14    print("Invalid JSON")
15else:
16    save_to_database(data)  # Exceptions here propagate normally

Without else, the except clause covers both json.loads and save_to_database. With else, only the parsing step is guarded, and any unexpected exceptions from save_to_database propagate up the call stack where they belong.

Common Use Cases

python
1# 1. File operations
2try:
3    f = open("config.json")
4except FileNotFoundError:
5    config = default_config()
6else:
7    config = json.load(f)
8    f.close()
9
10# 2. Dictionary key access
11try:
12    value = data_dict[key]
13except KeyError:
14    print(f"Key {key!r} not found")
15else:
16    process_value(value)
17
18# 3. Type conversion
19try:
20    number = int(user_input)
21except ValueError:
22    print("Please enter a valid number")
23else:
24    if number > 0:
25        print(f"Processing {number}")
26
27# 4. Network requests
28import requests
29
30try:
31    response = requests.get(url, timeout=5)
32    response.raise_for_status()
33except requests.RequestException as e:
34    log_error(e)
35    result = cached_fallback()
36else:
37    result = response.json()

else vs Putting Code at the End of try

python
1# Option A: code at end of try block
2try:
3    conn = database.connect()
4    result = conn.execute(query)   # Both lines are guarded
5except DatabaseError:
6    handle_error()
7
8# Option B: use else to separate
9try:
10    conn = database.connect()      # Only this is guarded
11except DatabaseError:
12    handle_error()
13else:
14    result = conn.execute(query)   # This runs unguarded

Option A catches DatabaseError from both connect() and execute(). Option B catches it only from connect(). If execute() fails, the exception propagates — which may be the correct behavior if you want to handle connection errors differently from query errors.

else with finally

python
1import sqlite3
2
3def fetch_user(user_id):
4    conn = None
5    try:
6        conn = sqlite3.connect("app.db")
7        cursor = conn.execute(
8            "SELECT * FROM users WHERE id = ?", (user_id,)
9        )
10    except sqlite3.Error as e:
11        print(f"Database error: {e}")
12        return None
13    else:
14        user = cursor.fetchone()
15        return dict(user) if user else None
16    finally:
17        if conn:
18            conn.close()  # Always close the connection

The finally block runs after either except or else, making it ideal for cleanup. The else block processes the successful result, while finally handles resource cleanup regardless of outcome.

When NOT to Use else

python
1# Unnecessary — the try block is simple and self-contained
2try:
3    value = int(input("Enter a number: "))
4except ValueError:
5    value = 0
6# Just use value here — no need for else
7
8# Also unnecessary when except re-raises or returns
9try:
10    data = json.loads(text)
11except json.JSONDecodeError:
12    raise InvalidInputError("Bad JSON")
13# If we get here, data is valid — else adds no clarity
14process(data)

Skip else when the except block returns, raises, or breaks out of the flow. In these cases, code after the try/except only executes on success anyway.

The EAFP Principle

Python follows "Easier to Ask Forgiveness than Permission" (EAFP), which favors try/except over pre-checking conditions:

python
1# LBYL (Look Before You Leap) — non-Pythonic
2if key in dictionary:
3    value = dictionary[key]
4    process(value)
5
6# EAFP with else — Pythonic
7try:
8    value = dictionary[key]
9except KeyError:
10    handle_missing_key()
11else:
12    process(value)

The else block complements EAFP by clearly separating the "risky" operation from the "success" path.

Common Pitfalls

  • Catching too broad an exception without else: Using except Exception with code after the risky call inside try silently swallows errors from non-risky code. Move non-risky code to else so only the intended operation is guarded.
  • Confusing else with finally: else runs only on success (no exception). finally runs always (exception or not). Putting cleanup code in else means it is skipped when an exception occurs, which is usually wrong for cleanup.
  • Using else when except already exits: If the except block calls return, raise, or sys.exit(), code after the try/except only runs on success. Adding else is redundant and reduces readability.
  • Bare except with else: except: (no exception type) catches everything including KeyboardInterrupt and SystemExit. This makes the else block unreachable for many real errors. Always specify the exception type.
  • Variable scoping confusion: Variables assigned in the try block are accessible in else (Python does not create a new scope for try). But if the exception occurs before the assignment, the variable is undefined in else, causing NameError.

Summary

  • try-except-else is a recommended Python pattern that narrows exception handling scope
  • The else block runs only when try completes without exceptions
  • Use else to separate risky operations (in try) from success-path logic (in else)
  • Combine with finally for cleanup that must run regardless of outcome
  • Skip else when except already exits the flow (return, raise, break)
  • The pattern aligns with Python's EAFP philosophy — try the operation, handle failure, then proceed on success

Course illustration
Course illustration