Handling Exception

By the Syncfusion DevTutorials team · Last updated

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:

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.

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.

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.

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:

CauseWhat it looks like
A missing :if marks >= 35 with no colon
A missing closing bracketprint("Asha"
Wrong indentationA line inside an if that is not indented
Using = where == was meantif marks = 78:
A keyword used as a nameclass = "7A"

Examples

Wrong indentation

marks = 78

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

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').

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 errorException
When Python noticesBefore anything runsWhile running
How much of the file runsNone of itEverything up to the failing line
Can you catch it with tryNoYes
Caused byThe way the code is writtenThe values the code met
ExampleA 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

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.

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:

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.

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.

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.

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.

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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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 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__.

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 byPython, automaticallyYou, with raise ... from
MeansThis happened while that was being handledThat caused this
When it is NoneNothing was being handledYou 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.

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.

MistakeWhat you seeWhy it happensFix
try around a syntax errorThe file does not run at allSyntax is checked before anything runsFix the syntax; try cannot help
except: with no typeA bug disappears into "something went wrong"A bare except catches everythingName the exception you expect
General type listed firstThe specific except never runsThe first matching block winsMost specific first, general last
except ValueError, TypeError:SyntaxErrorThat is the old Python 2 spellingexcept (ValueError, TypeError):
Printing error and carrying on with a broken valueA later, stranger exceptionThe handler reported but did not recoverReturn a default, skip the row, or re-raise
Risky and safe lines in one tryThe wrong cause is reportedA mistake in the safe line is caught tooMove the safe lines into else
Expecting finally to be skipped by returnThe finally line prints anywayfinally runs on every way outExpect it; that is its job
assert used to check user inputThe check vanishes when assertions are offassert is a development toolUse raise
raise ValueError without bracketsIt works, but there is no messageThe class was raised, not an instanceraise ValueError("what went wrong")
from None used by habitThe real cause is missing from the report__suppress_context__ hides itUse from error, or nothing at all

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