# Exceptions in Python: try, except, else, finally, raise and Custom Exceptions

URL: https://skillaria.top/en/guides/python/python-exceptions-try-except-raise

An exception in Python is an error object that interrupts the normal flow of code until a try/except block catches it; else runs only if no error occurred, and finally always runs. You raise an exception yourself with the raise statement, for example raise ValueError("amount is negative"), and create your own with a class that inherits from Exception.

An exception in Python is an object that signals an error and interrupts the normal flow of a program. If nobody catches it, Python prints a traceback and stops. Exceptions are caught with the `try/except` construct and raised with the `raise` statement. Let us look at how this works, which errors occur most often and how to avoid turning error handling into hiding errors.

## What an exception is in Python

When an operation cannot complete — division by zero, access to a missing key, turning a word into a number — Python creates an exception object and "throws" it. The current function stops, and the exception travels up the chain of calls until it meets a suitable handler. If none is found, the program crashes.

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

By the way, the answer to a common question: dividing by zero in Python raises a `ZeroDivisionError` exception. This applies to `/`, `//` and `%` alike.

## How to read a traceback

A traceback is a report of how the program arrived at the error. It is easier to read from the bottom up. The last line is the main thing: the exception type and the message. Above it is the line of code where it happened, and higher still is who called it, all the way to the entry point.

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'hundred'
```

Since Python 3.11, markers such as `~~~^^^` are drawn under the line of code (PEP 657): they show exactly which expression in the line failed. This helps especially when several calls follow one another on the same line.

### "Did you mean" hints

Since Python 3.10, a typo in a name produces an error that suggests a close alternative. In Python 3.15 the hints have become smarter: the interpreter knows the habits from other languages. Calling `list.push` will suggest `append`, and `"coffee".toUpperCase()` will suggest the `upper` method. The hint also looks into nested attributes: if a dictionary lives in the `totals` attribute of an object and you call `items()` on the object itself, the error will suggest `.totals.items`. `delattr()` now also suggests close names.

## Exception types in Python

There are dozens of built-in exceptions, but at the start you will most often see these:

| Exception | When it occurs | Example |
| --- | --- | --- |

| `NameError` | A name is not defined, most often a typo | `print(totl)` |

| `TypeError` | The operation does not suit this type | `"5" + 3` |

| `ValueError` | The type is right, the value is not | `int("hundred")` |

| `KeyError` | There is no such key in the dictionary | `d["missing"]` |

| `IndexError` | The index is outside the list | `[1, 2][5]` |

| `AttributeError` | The object has no such method or attribute | `[].push(1)` |

| `ZeroDivisionError` | Division by zero | `1 / 0` |

| `FileNotFoundError` | The file was not found | `open("nope.txt")` |

`SyntaxError` deserves a separate mention (and its special case `IndentationError`): it occurs while the file is being parsed, before the code runs, so you cannot catch it with `try` in the same file.

### The exception hierarchy

Exceptions are classes, and they are arranged in a hierarchy. At the top is `BaseException`. It is inherited by `Exception` (almost all "ordinary" errors) and several system ones: `KeyboardInterrupt`, `SystemExit`, `GeneratorExit`.

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

The hierarchy matters when catching: `except LookupError` will catch both `KeyError` and `IndexError`. And `KeyboardInterrupt` is not a bug in the code: it is how Python reports that the user pressed Ctrl+C. It deliberately does not inherit from `Exception`, so that `except Exception` does not prevent you from stopping the program.

## Handling exceptions: try/except/else/finally

Code that may fail goes into a `try` block. The `except` block names the error type and what to do with it. The `except` block is exactly what Python uses for handling exceptions.

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"Skipping the line: {e}")
        return None
    else:
        return value          # runs if there was no error
    finally:
        print("parsing finished")   # always runs

print(parse_amount("250"))
```

The output order here is: first "parsing finished", then `250.0`. The `return` in the `else` block remembers the value, then Python necessarily runs `finally` and only after that returns the result. That is why `finally` is reliable for cleanup: closing a file, releasing a connection.

If you write your own `return` in `finally`, it overrides the previous one and can even "swallow" the exception. Since Python 3.14 such code triggers a `SyntaxWarning` (PEP 765) — do not write `return` in `finally`.

### Several exceptions at once

You can write several `except` blocks for different types, or list the types in a tuple:

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("Invalid amount:", e)
    amount = 0
```

The blocks are checked from top to bottom, and the first matching one runs, so more specific types go above general ones. Since Python 3.14 (PEP 758) several types can be listed without brackets — `except ValueError, TypeError:` — but if there is an `as` clause, the brackets are still required.

### How to print the error message and find its type

The exception object is available through `as e`. `str(e)` gives the message, `type(e).__name__` gives the name of the type:

```
try:
    int("hundred")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'hundred'
```

## The traceback module: the full error text

Sometimes you need to catch an error but keep the full traceback — for a log or a report. The `traceback` module is made for this:

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # prints the traceback to stderr
    text = traceback.format_exc()     # the same text as a string
```

If you write logs with the `logging` module, it is simpler to call `logging.exception("could not calculate the total")` inside `except` — the traceback is added to the message automatically.

## Python raise: how to raise an exception

The `raise` statement throws an exception manually. This is how a function reports that it cannot work with such data:

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"The amount cannot be negative: {amount}")
    ...

add_expense(-5)
# ValueError: The amount cannot be negative: -5
```

Python raise is a statement, not a function, so no brackets are needed after the word `raise`: the brackets contain the arguments passed to the exception constructor. Choose the type by meaning: a wrong value is `ValueError`, a wrong type is `TypeError`.

### raise without arguments and raise from

Inside `except`, a bare `raise` re-throws the current exception unchanged. You need this when you want to log something but leave the handling to the calling code.

If you replace one exception with another, use `raise ... from e` — the original error is kept in the chain, and the traceback shows both:

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"Config file not found: {path}") from e
```

### add_note: adding context to an error

Since Python 3.11 (PEP 678) you can attach a note to an exception with the `add_note()` method and re-raise it. The error type does not change, and the note is printed below the message:

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"file expenses.csv, line {line_no}")
        raise

parse("hundred", 17)
# ValueError: could not convert string to float: 'hundred'
# file expenses.csv, line 17
```

## How to create your own exception in Python

Custom exceptions are needed for errors of your problem domain: the budget is exceeded, the order is already paid, the user is blocked. A subclass of `Exception` is enough:

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: limit {limit}, spent {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("Food", 10000, 12500)
except BudgetExceeded as e:
    print("Warning:", e)   # Warning: Food: limit 10000, spent 12500
```

Now the calling code catches exactly the budget overrun and does not confuse it with typos. In a large project it is handy to create a common base class (`class TrackerError(Exception)`) and inherit all your exceptions from it — then they can all be caught with a single `except TrackerError`. Inherit from `Exception`, not from `BaseException`.

## Exception groups and except*

Sometimes several errors happen at once — for example when checking a whole file, or in parallel tasks of `asyncio.TaskGroup`. Since Python 3.11 they can be collected into an `ExceptionGroup` and handled by type with `except*`:

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("errors in the file", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"Invalid rows: {len(eg.exceptions)}")   # Invalid rows: 2
```

## Python try except all errors: why you should not do this

The most harmful habit is catching everything: `except: pass`. The program "stops crashing", but in fact it stops reporting problems. A bare `except` even catches `KeyboardInterrupt` and `SystemExit`, so the program cannot be stopped with Ctrl+C. `except Exception: pass` is slightly better, but it still silently swallows typos in names and logic errors.

How to handle exceptions correctly in Python:

- catch only the type you know how to handle: `except ValueError`, not everything;
- keep the `try` block short — only the lines that can fail in that particular way;
- if you catch broadly (`except Exception`) at the top level of a program, always log the traceback;
- do not use exceptions for ordinary branching where a check is enough: `dict.get()` instead of `try/except KeyError` in simple cases;
- for resources prefer `with` — it closes the file itself even when an error occurs.

## Summary

Exceptions in Python are a normal mechanism for reporting errors, not a reason to hide them. Read the traceback from the bottom up, catch specific types in `try/except`, use `else` for code that runs "if everything went well" and `finally` for cleanup. Python raise lets you report an error yourself, while `raise from` and `add_note` preserve context, and your own exceptions inherit from `Exception`. In our Python course this topic is practised hands-on: the expense tracker stops crashing because of one broken CSV line and expresses a budget overrun with its own exception.

## FAQ

**How is an error different from an exception in Python?**

In everyday speech the two are synonyms. Technically, a SyntaxError is detected while the code is parsed, while other errors occur during execution as exception objects that can be caught.

**Can execution continue from the same place after an exception?**

No. After it is caught, execution continues after the try/except block, not from the line where the error occurred. If you need to process each line of a file, put the try inside the loop.

**What happens if an exception occurs inside except?**

It propagates further, and the original exception is kept in its __context__ attribute. The traceback will show the phrase "During handling of the above exception, another exception occurred".

**How do I check in tests that a function raises an exception?**

In pytest use the construct with pytest.raises(ValueError):, in unittest use self.assertRaises(ValueError). The test passes only if an exception of the right type occurred inside the block.

**What is assert and how does it differ from raise?**

assert condition, "message" raises AssertionError if the condition is false. It is a development check tool: when run with the -O flag, asserts are disabled, so user input must be validated with if and raise.


---
Source: Skillaria.Top (https://skillaria.top)
