# Handling Exception

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Exception Handling
Source: https://www.devtutorials.ai/learn/python/exception-handling/handling-exception

> Syntax errors against exceptions, the ten you meet first, try-except-else-finally, raising, custom exception types and exception chaining.

A school office has a rule for marks: every mark must be a whole number between 0 and 100.

One day a teacher types `seventy` instead of `78`. The program that adds up the marks stops dead, and the whole report fails — because of one cell in one row.

That is the difference between a program that works and a program you can trust. A trustworthy program expects things to go wrong, notices when they do, and keeps going.

Here is that idea in action — one program that trusts every value, and one that expects bad ones:

```python runnable title="One bad value, two different programs"
marks = ["78", "85", "seventy", "60"]

total = 0
for text in marks:
    try:
        total += int(text)
    except ValueError:
        print("Skipping a mark that is not a number:", text)

print("Total:", total)
```

An **exception** is Python's way of saying "I cannot carry on with this line". Handling it means deciding what should happen instead, rather than letting the program stop.

## What You Will Learn

In this lesson, you will learn:

- The difference between an error Python finds before running and one it meets while running
- The ten exceptions you will actually see as a beginner, and what each one means
- How `try` and `except` work, line by line
- What `else` and `finally` add, and when `finally` runs
- How to catch several different problems, and in which order
- How to raise an exception yourself, and how to write your own exception type
- What `raise ... from` really changes, and why the two tracks `__cause__` and `__context__` exist

By the end of this lesson, you will be able to tell a syntax error from an exception, recognise the common exception types, handle each one with `try`, `except`, `else` and `finally`, raise exceptions of your own, and write custom exception types for your program's own rules.

## Errors

### What Are Errors?

An **error** is anything that stops Python from doing what the line asks. There are only two kinds, and the difference is *when* Python notices.

- A **syntax error** is found *before* a single line runs. Python reads the whole file first, and if it cannot understand the shape of the code, nothing runs at all.
- An **exception** is found *while* the program runs. The code made sense, but the values it met did not work.

```python runnable expect-error title="The code is fine; the value is not"
print("Adding the marks")
print(int("78") + 1)
print(int("seventy") + 1)
print("This line never runs")
```

Wait — that cell did run the first two lines. Look at the output. Python printed, printed again, and *then* stopped. That is the signature of an exception: the program was already running.

### Why Errors Occur in Programs

Errors are not a sign of a bad programmer. They come from the gap between what your code assumes and what the world actually hands it.

- A person types something unexpected.
- A file that should be there has been moved or renamed.
- A number is zero when the code was going to divide by it.
- A list is empty when the code reaches for its first item.
- A key that "always exists" is missing for one student.

Every one of these is outside your control. What is inside your control is what happens next.

### Importance of Error Handling

Without handling, one bad row ends the whole run and the reader sees a wall of red text they did not write. With handling, the program reports that one row and finishes the other thirty-nine.

```python runnable title="One bad row should not lose thirty-nine good ones"
rows = [("Asha", "78"), ("Ravi", "85"), ("Meera", "none"), ("Dev", "60")]

for name, text in rows:
    try:
        print(name, "scored", int(text))
    except ValueError:
        print(name, "has no usable mark — please check the sheet")

print("Report finished")
```

A good rule: catch the problems you can do something sensible about, and let the rest stop the program loudly. A bug you hide is a bug you will meet again later, further from its cause.

## Syntax Errors

### What Are Syntax Errors?

A **syntax error** means the text is not Python. Python cannot even begin, so nothing in the file runs — not even the correct lines above the broken one.

```python runnable expect-error title="A missing colon stops the whole file"
print("Starting the report")

marks = 78
if marks >= 35
    print("Pass")
```

Notice that `Starting the report` was never printed. With a syntax error there is no partial run.

### Common Causes

Each of these five mistakes stops the whole file:

| Cause | What it looks like |
|---|---|
| A missing `:` | `if marks >= 35` with no colon |
| A missing closing bracket | `print("Asha"` |
| Wrong indentation | A line inside an `if` that is not indented |
| Using `=` where `==` was meant | `if marks = 78:` |
| A keyword used as a name | `class = "7A"` |

### Examples

```python runnable expect-error title="Wrong indentation"
marks = 78

if marks >= 35:
print("Pass")
```

```python runnable expect-error title="Assignment inside a condition"
marks = 78

if marks = 78:
    print("Exactly seventy-eight")
```

The message names the line and usually points at the exact character. Read it from the bottom up: the last line of the message is the useful one.

## What Is an Exception?

### Definition

An **exception** is an object Python creates when a line cannot be carried out. It has a **type** (such as `ValueError`) and a **message** (such as `invalid literal for int() with base 10: 'seventy'`).

```python runnable title="An exception is an object with a type and a message"
try:
    int("seventy")
except ValueError as error:
    print(type(error).__name__)
    print(error)
    print(error.args)
```

The word `as error` gives the exception object a name, so you can look at it.

### How Exceptions Occur

Python *raises* the exception at the line that failed. If nothing catches it, it travels up through every function that was waiting, and each one stops too. When it reaches the top, the program ends and the message is printed.

### Error vs Exception

People use both words loosely, so here is the honest version in one table.

| | Syntax error | Exception |
|---|---|---|
| When Python notices | Before anything runs | While running |
| How much of the file runs | None of it | Everything up to the failing line |
| Can you catch it with `try` | No | Yes |
| Caused by | The way the code is written | The values the code met |
| Example | A missing `:` | `int("seventy")` |

A `try` block cannot rescue a syntax error. By the time your `try` would run, Python has already refused to read the file. Fix the syntax; handle the exceptions.

## Built-in Exceptions

Python ships with a family of exception types. Every one of them is a class, and they all sit under `Exception`.

```python runnable title="They are classes, in a family"
print(ZeroDivisionError.__mro__)
print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(KeyError, LookupError))
print(issubclass(ValueError, Exception))
```

`__mro__` lists a class and everything above it. Reading that first line: a `ZeroDivisionError` is an `ArithmeticError`, which is an `Exception`. This matters because `except ArithmeticError` will catch a `ZeroDivisionError` too.

### Common Exceptions at a Glance

Rather than memorising definitions, it helps to see the actual messages Python produces. Here are the ten you will meet first, each raised for real.

```python runnable title="Six of them, one after another"
totl = 0
del totl

jobs = ["name", "type", "value", "zero", "index", "key"]

for job in jobs:
    try:
        if job == "name":
            print(totl)
        elif job == "type":
            print("78" + 1)
        elif job == "value":
            print(int("seventy"))
        elif job == "zero":
            print(240 / 0)
        elif job == "index":
            print([78, 85][5])
        elif job == "key":
            print({"Asha": 78}["Ravi"])
    except Exception as error:
        print(type(error).__name__ + ":", error)
```

The first two lines of that cell create `totl` and then delete it, so that the loop really does read a name that is gone. Every message in the output came from Python itself, not from the lesson.

### NameError

Raised when a name is read before anything has been stored in it. Nine times out of ten it is a spelling mistake.

```python runnable title="NameError"
total = 78
del total

try:
    print(total)
except NameError as error:
    print("NameError:", error)
```

### TypeError

Raised when the types do not fit the operation — text added to a number, a function called with the wrong number of arguments.

```python runnable title="TypeError"
try:
    print("Asha scored " + 78)
except TypeError as error:
    print("TypeError:", error)

print("Asha scored " + str(78))
```

### ValueError

The type is right but the value is not. `int()` is given text that is not a number.

```python runnable title="ValueError"
for text in ["78", "seventy", "78.5"]:
    try:
        print(int(text))
    except ValueError as error:
        print("ValueError:", error)
```

`"78.5"` fails too. `int()` wants whole-number text; `float("78.5")` is what you want there.

### ZeroDivisionError

Dividing by zero. It appears most often as an average over an empty list.

```python runnable title="ZeroDivisionError"
marks = []

try:
    print(sum(marks) / len(marks))
except ZeroDivisionError as error:
    print("ZeroDivisionError:", error)
    print("Average of no marks is not a number")
```

### IndexError

A position that is past the end of a list, tuple or string.

```python runnable title="IndexError"
marks = [78, 85, 60]

try:
    print(marks[3])
except IndexError as error:
    print("IndexError:", error)

print("The last position is", len(marks) - 1)
```

### KeyError

A dictionary key that is not there.

```python runnable title="KeyError"
marks = {"Asha": 78, "Ravi": 85}

try:
    print(marks["Meera"])
except KeyError as error:
    print("KeyError:", error)

print(marks.get("Meera", "not enrolled"))
```

The message for a `KeyError` is just the key, in quotes. That is why it looks so bare.

### AttributeError

Asking an object for something it does not have. Usually a method from another language, or from another type.

```python runnable title="AttributeError"
name = "Asha"

try:
    name.push("!")
except AttributeError as error:
    print("AttributeError:", error)

print(name + "!")
```

### ImportError

The module was found, but the name you asked for is not inside it.

```python runnable title="ImportError"
try:
    from math import squareroot
except ImportError as error:
    print("ImportError:", error)

from math import sqrt
print(sqrt(81))
```

### ModuleNotFoundError

The module itself does not exist. It is a kind of `ImportError`, so `except ImportError` catches it as well.

```python runnable title="ModuleNotFoundError"
try:
    import schoolhelpers
except ModuleNotFoundError as error:
    print("ModuleNotFoundError:", error)
    print("The missing module was named:", error.name)

print(issubclass(ModuleNotFoundError, ImportError))
```

### FileNotFoundError

Opening a file that is not there. This one is worth meeting early, because file names are easy to get wrong.

```python runnable title="FileNotFoundError"
try:
    with open("attendance_sheet.txt") as sheet:
        print(sheet.read())
except FileNotFoundError as error:
    print("FileNotFoundError:", error.filename, "was not found")
```

Checking first does not help as much as you would hope. Between a check that says the file exists and the line that opens it, the file can be deleted. Handling the exception covers both cases, so handle rather than check.

## Exception Handling

### What Is Exception Handling?

**Exception handling** means writing, in advance, what should happen if a particular line fails. You mark the risky lines, and you say what to do instead.

### Why It Matters

Three reasons, in order of importance:

1. The program finishes instead of stopping halfway.
2. The person using it gets a sentence they understand, not a traceback.
3. The failure is recorded where you decided, not wherever it happened to land.

## try-except

Now for the four lines that do the handling. Each has a fixed job.

### Syntax

```text
try:
    the risky lines
except SomeError:
    what to do instead
```

### How It Works

Python runs the `try` block. If every line works, the `except` block is skipped entirely. If a line raises an exception of the named type, Python stops the `try` block *at that line* and jumps into the `except` block.

```python runnable title="The rest of the try block is skipped"
try:
    print("Reading the mark")
    mark = int("seventy")
    print("This line is never reached")
except ValueError:
    print("That mark was not a number")

print("Carrying on")
```

### A Worked Example

Here is a function that survives a bad argument, on a real pattern — averaging marks:

```python runnable title="A safe average"
def average(marks):
    try:
        return sum(marks) / len(marks)
    except ZeroDivisionError:
        return 0

print(average([78, 85, 60]))
print(average([]))
```

### Catching the Object Itself

Sometimes knowing that an exception happened is not enough. You may also need the exception object itself so you can inspect its type or message.

```python runnable title="Naming the exception with as"
try:
    marks = {"Asha": 78}
    print(marks["Ravi"])
except KeyError as error:
    print("Type:", type(error).__name__)
    print("Message:", error)
```

`except:` with no type at all catches *everything*, including a typing mistake of your own and the key press that interrupts the program. It turns bugs into silence. Name the exception you expect, or use `except Exception` if you really must catch broadly.

```python runnable title="A bare except hides the real problem"
try:
    total = int("seventy") + 1
except:
    print("Something went wrong")
```

That message tells nobody anything. Compare it with `except ValueError as error: print(error)`, which names the value that broke.

## else and finally

Two more blocks may follow `try` and `except`, each with its own fixed job.

### The else Block

The `else` block runs **only when the `try` block finished without raising**. It is for the lines that should run on success but that you do not want protected by the same `try`.

```python runnable title="else runs only on success"
def divide(total, count):
    try:
        share = total / count
    except ZeroDivisionError:
        print("There are no students to share between")
    else:
        print("Each student gets", share)

divide(240, 3)
divide(240, 0)
```

Why not just put that `print` inside the `try`? Because then a mistake *inside the print* would be caught by `except ZeroDivisionError` too, and you would be told the wrong story.

### The finally Block

The `finally` block runs **always** — after success, after a handled exception, and on the way out of an unhandled one. It is for cleaning up: closing a file, releasing something you borrowed.

```python runnable title="finally runs on every path"
def read_mark(text):
    try:
        return int(text)
    except ValueError:
        return 0
    finally:
        print("  (finished checking", text + ")")

print(read_mark("78"))
print(read_mark("seventy"))
```

Read that output carefully. The `finally` line prints *before* the returned value is printed — because `finally` runs as the function leaves, before the caller gets the value back. Even a `return` cannot skip it.

### All Four Together

Here are all four blocks in one function:

```python runnable title="try, except, else, finally"
def report(text):
    print("Mark:", text)
    try:
        mark = int(text)
    except ValueError:
        print("  not a number")
    else:
        print("  pass" if mark >= 35 else "  fail")
    finally:
        print("  done")

report("78")
report("20")
report("seventy")
```

The order is fixed: `try`, then any `except` blocks, then `else`, then `finally`.

## Handling Multiple Exceptions

One `try` block will often need to cope with more than one kind of problem. There are two ways to arrange that.

### Separate except Blocks

One `try` can have several `except` blocks. Python uses the **first one that matches**, and only that one.

```python runnable title="A different message for each problem"
for text in ["78", "seventy", None]:
    try:
        print("Next mark:", int(text) + 1)
    except ValueError:
        print("That is text, but not a number:", text)
    except TypeError:
        print("That is not text at all:", text)
```

### One Block for Several Types

If the response is the same, list the types in brackets — a tuple.

```python runnable title="A tuple of exception types"
for text in ["78", "seventy", None]:
    try:
        print(int(text) + 1)
    except (ValueError, TypeError) as error:
        print(type(error).__name__ + ":", error)
```

### Order Matters

Because the first match wins, a general type placed above a specific one will swallow it.

```python runnable title="The general type first catches everything"
try:
    print(240 / 0)
except ArithmeticError:
    print("Caught as ArithmeticError — the block below never gets a turn")
except ZeroDivisionError:
    print("This line is unreachable")
```

Always put the **most specific** exception first and the most general last. Python does not warn you about an unreachable `except`, so a misordered pair looks perfectly fine until you need the specific message.

## Raising Exceptions

So far the exceptions have been Python's idea. You can raise them yourself too, and this is how a function refuses bad input in the first place.

### The raise Statement

You can raise an exception yourself with `raise`. This is how a function refuses bad input instead of quietly carrying on with it.

```python runnable title="Refusing a mark that makes no sense"
def set_mark(mark):
    if not isinstance(mark, int):
        raise TypeError("mark must be a whole number")
    if not 0 <= mark <= 100:
        raise ValueError("mark must be between 0 and 100")
    print("Mark set to", mark)

set_mark(78)

for bad in ["78", 145]:
    try:
        set_mark(bad)
    except (TypeError, ValueError) as error:
        print(type(error).__name__ + ":", error)
```

Choose the type that fits: `TypeError` for the wrong kind of thing, `ValueError` for the right kind with an impossible value.

### Raising with a Message

Whatever you pass to the exception becomes its message. Write it for the person who will read it, and say what was expected.

```python runnable title="Two messages, one useful"
def check(mark):
    if mark > 100:
        raise ValueError(f"mark {mark} is above the maximum of 100")

try:
    check(145)
except ValueError as error:
    print(error)
```

### Re-raising

A bare `raise` inside an `except` block throws the *same* exception onward, after you have done something about it. Use it when you want to note the problem but not to solve it.

```python runnable title="Note it here, let it carry on"
try:
    try:
        marks = {"Asha": 78}
        print(marks["Ravi"])
    except KeyError as error:
        print("Inner: a student is missing from the sheet:", error)
        raise
except KeyError as error:
    print("Outer: the report cannot be built without", error)
```

### assert

`assert` raises an `AssertionError` when a condition you believed is false. It is for catching your own mistakes while developing, not for checking what a user typed.

```python runnable title="assert states something you believe"
try:
    mark = 145
    assert 0 <= mark <= 100, "mark out of range"
except AssertionError as error:
    print("AssertionError:", error)
```

Python can be run with assertions switched off, and then every `assert` line disappears. Never use `assert` to validate input or to enforce a rule the program depends on — use `raise`.

## Custom Exceptions

When your program has rules of its own, `ValueError` is not always specific enough. That is what custom exceptions are for.

### Why Write Your Own

Because `ValueError` tells the caller nothing about *your* program. A `MarkOutOfRange` does, and it lets a caller catch exactly that problem without also catching every other `ValueError` in the block.

### Defining One

A custom exception is a class that inherits from `Exception`. The smallest useful one is three words long.

```python runnable title="The smallest custom exception"
class SchoolError(Exception):
    pass

try:
    raise SchoolError("the attendance sheet is locked")
except SchoolError as error:
    print(type(error).__name__ + ":", error)
```

### A Family of Them

Give your program one base exception and build the specific ones under it. Then a caller can catch one kind, or all of them, with one name.

```python runnable title="One base, two specific types"
class SchoolError(Exception):
    pass

class MarkOutOfRange(SchoolError):
    def __init__(self, mark):
        self.mark = mark
        super().__init__(f"mark {mark} is not between 0 and 100")

class StudentNotEnrolled(SchoolError):
    pass

def check(name, mark):
    if name not in {"Asha", "Ravi", "Meera"}:
        raise StudentNotEnrolled(f"{name} is not in this class")
    if not 0 <= mark <= 100:
        raise MarkOutOfRange(mark)
    return mark

print(check("Asha", 78))

for name, mark in [("Asha", 145), ("Dev", 60)]:
    try:
        check(name, mark)
    except SchoolError as error:
        print(type(error).__name__ + ":", error)
```

That last `except SchoolError` catches both, because both inherit from it. Storing `self.mark` on the exception means the handler can use the value, not just read about it.

Name a custom exception for the *rule that was broken*, not for the place it happened. `MarkOutOfRange` still makes sense when you move the check to another file; `CheckMarksFunctionError` does not.

## Nested Exceptions

A `try` can sit inside another `try`, or inside the `except` of another. The inner handler gets the first chance; anything it does not catch travels outward.

```python runnable title="Inner first, then outer"
def average(marks):
    try:
        try:
            return sum(marks) / len(marks)
        except ZeroDivisionError:
            raise ValueError("no marks were given")
    finally:
        print("  (average attempted)")

print(average([78, 85]))

try:
    average([])
except ValueError as error:
    print("Caught outside:", error)
```

Follow the second call: the division fails, the inner `except` turns it into a `ValueError`, `finally` runs on the way out, and the outer `try` catches what is left. Each layer handles what it understands and passes on what it does not.

Keep nesting shallow. Two levels is usually the most a reader can follow; beyond that, a small function with its own `try` is clearer.

## Exception Chaining

When one exception is raised while another is being handled, Python keeps both. This is **chaining**, and it exists so that a tidy message never hides the real cause.

### The Implicit Chain: `__context__`

Any exception raised inside an `except` block automatically records the one it interrupted, in `__context__`.

```python runnable title="Python remembers what it interrupted"
try:
    try:
        print(240 / 0)
    except ZeroDivisionError:
        print(int("seventy"))
except ValueError as error:
    print("The new exception:", type(error).__name__)
    print("It interrupted:", type(error.__context__).__name__)
    print("Declared cause:", error.__cause__)
```

Note that `__cause__` is `None`. Python filled in the context by itself, but nobody *said* the two were related.

### The Explicit Chain: `raise ... from`

Writing `raise New(...) from error` says it outright: this new exception was caused by that one. It sets `__cause__`.

```python runnable title="raise ... from names the cause"
class MarkError(Exception):
    pass

try:
    try:
        mark = int("seventy")
    except ValueError as error:
        raise MarkError("mark must be a number") from error
except MarkError as error:
    print(error)
    print("Cause:", repr(error.__cause__))
    print("Context:", repr(error.__context__))
```

Both are filled in now. In a real traceback this is the difference between *During handling of the above exception, another exception occurred* and *The above exception was the direct cause of the following exception* — a guess against a statement.

### `__cause__` and `__context__` Side by Side

| | `__context__` | `__cause__` |
|---|---|---|
| Set by | Python, automatically | You, with `raise ... from` |
| Means | This happened while that was being handled | That caused this |
| When it is `None` | Nothing was being handled | You never said `from` |

### Hiding the Chain: `from None`

Sometimes the inner exception is noise — an implementation detail your caller should not have to read. `from None` hides it.

```python runnable title="from None drops the chain from the report"
try:
    try:
        mark = int("seventy")
    except ValueError:
        raise ValueError("mark must be a whole number between 0 and 100") from None
except ValueError as error:
    print(error)
    print("Cause:", error.__cause__)
    print("Context is still recorded:", type(error.__context__).__name__)
    print("But suppressed:", error.__suppress_context__)
```

Look closely: `__context__` is still there. `from None` only sets `__suppress_context__`, which tells the traceback printer not to show it. Nothing is destroyed — it is hidden from the report.

`from None` is the easiest way to lose the one clue that would have told you what really went wrong. Use it only when you are certain the inner exception adds nothing, and never while you are still hunting a bug.

## Common mistakes

Before moving on, here are some of the mistakes beginners make with exceptions.

Each row is one mistake. The middle column is what you will actually see on screen.

<div className="tscroll">

| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
| `try` around a syntax error | The file does not run at all | Syntax is checked before anything runs | Fix the syntax; `try` cannot help |
| `except:` with no type | A bug disappears into "something went wrong" | A bare `except` catches everything | Name the exception you expect |
| General type listed first | The specific `except` never runs | The first matching block wins | Most specific first, general last |
| `except ValueError, TypeError:` | `SyntaxError` | That is the old Python 2 spelling | `except (ValueError, TypeError):` |
| Printing `error` and carrying on with a broken value | A later, stranger exception | The handler reported but did not recover | Return a default, skip the row, or re-raise |
| Risky *and* safe lines in one `try` | The wrong cause is reported | A mistake in the safe line is caught too | Move the safe lines into `else` |
| Expecting `finally` to be skipped by `return` | The `finally` line prints anyway | `finally` runs on every way out | Expect it; that is its job |
| `assert` used to check user input | The check vanishes when assertions are off | `assert` is a development tool | Use `raise` |
| `raise ValueError` without brackets | It works, but there is no message | The class was raised, not an instance | `raise ValueError("what went wrong")` |
| `from None` used by habit | The real cause is missing from the report | `__suppress_context__` hides it | Use `from error`, or nothing at all |

</div>

## What to Remember

- **A syntax error is found before anything runs; an exception is found while running.** Only the second kind can be caught.
- An exception is an object with a type and a message. `as error` gives you a name for it.
- `try` holds the risky lines. Keep it as short as you can.
- `except` names the type you expect. `except:` on its own hides bugs.
- The **first matching `except` wins**, so write the specific types above the general ones.
- `else` runs only when the `try` block succeeded; `finally` runs on every path, even through a `return`.
- Several types sharing one response go in brackets: `except (ValueError, TypeError):`.
- `raise` makes an exception yourself; a bare `raise` inside `except` passes the same one onward.
- `assert` is for your own mistakes during development, never for checking input.
- A **custom exception** is a class inheriting from `Exception`. Give your program one base type and build the rest under it.
- `__context__` is filled in by Python; `__cause__` is filled in by you, with `raise ... from`.
- `from None` does not delete the context — it only hides it from the report.

Next: **File Handling**
