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
tryandexceptwork, line by line - What
elseandfinallyadd, and whenfinallyruns - 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 ... fromreally 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:
| 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
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 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.
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:
- The program finishes instead of stopping halfway.
- The person using it gets a sentence they understand, not a traceback.
- 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 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.
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.
| 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 |
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 errorgives you a name for it. tryholds the risky lines. Keep it as short as you can.exceptnames the type you expect.except:on its own hides bugs.- The first matching
exceptwins, so write the specific types above the general ones. elseruns only when thetryblock succeeded;finallyruns on every path, even through areturn.- Several types sharing one response go in brackets:
except (ValueError, TypeError):. raisemakes an exception yourself; a bareraiseinsideexceptpasses the same one onward.assertis 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, withraise ... from.from Nonedoes not delete the context — it only hides it from the report.
Next: File Handling