Python Custom Exception Classes
PCAP objective 2.2 is extend the exceptions hierarchy with self-defined exceptions, and it is a small objective with one genuinely useful idea: define a base exception for your code and derive the specific ones from it, so a caller can catch the whole family with one clause or any single member precisely. The mechanics are three lines of class definition, and the one detail worth checking is what super().__init__ does to args - which is not what most explanations say.
Exceptions in Depth Guide 3 of 25 Intermediate
- OSUbuntu 26.04 LTS
- Python3.14.4
- pip25.1.1
- TimeAbout 15 min
Nothing here is version-dependent. super() with no arguments has worked since Python 3.0; the Python 2 form super(Cls, self) still works and is not needed.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RUNNER01 | 192.168.0.27 | Ubuntu 26.04 LTS | Python 3.14.4 - the only machine this path needs | 2 Core | 4 GB | 50 GB |
This guide includes
Use this for the objective on extending the exception hierarchy. This matters because the point is not a new name - it is a family that one base class catches, which is what the code around you actually depends on.
- writing the smallest useful custom exception, in three lines
- building a family caught by its base, which is the point of the objective
- carrying extra data on an exception object
- finding what
super().__init__actually controls - learning what cannot be raised, and how an uncaught custom exception looks
Before you start
- guide 23 - a clause catches subclasses.
- guide 2 - what
argsis. - This guide uses classes before guide 12 teaches them. Three lines is all you need, and the OOP track covers the rest.
-
The smallest useful custom exception
Three lines: a class, inheriting from
Exception, withpassas its body. It needs nothing else - everything an exception does is inherited.bash Example session cat > ~/py/customsimple.py <<'PY'class AppError(Exception): pass try: raise AppError("something went wrong in the application")except AppError as e: print(type(e).__name__, "|", e, "| args", e.args) print("AppError is an Exception:", issubclass(AppError, Exception))PYpython3 ~/py/customsimple.pyAppError | something went wrong in the application | args ('something went wrong in the application',)AppError is an Exception: TrueExpected result
AppError | something went wrong in the application | args ('something went wrong in the application',), thenTrue.Success conditionYou can define and raise an exception of your own.
-
A family, caught by its base
This is the actual point of the objective. Define one base exception for your code and derive the specific ones from it. A caller can then catch all of them with one clause, or any single one precisely, without knowing your full list.
bash Example session cat > ~/py/customfamily.py <<'PY'class AppError(Exception): pass class NotFound(AppError): pass class Forbidden(AppError): pass for exc in (NotFound("alpha"), Forbidden("beta"), AppError("gamma")): try: raise exc except AppError as e: print("caught", type(e).__name__, "with the AppError clause:", e)PYpython3 ~/py/customfamily.pycaught NotFound with the AppError clause: alphacaught Forbidden with the AppError clause: betacaught AppError with the AppError clause: gammaExpected resultAll three -
NotFound,ForbiddenandAppErroritself - caught by the oneexcept AppError:clause.Success conditionYou can design an exception family a caller can use at either level.
-
Carrying extra data
An exception is an object, so it can hold anything useful. Define
__init__, callsuper().__init__with the message, and store the rest onself.bash Example session cat > ~/py/customdata.py <<'PY'class NotFound(Exception): def __init__(self, key): super().__init__("no such key: " + key) self.key = key try: raise NotFound("alpha")except NotFound as e: print("message ", e) print("e.key ", e.key) print("e.args ", e.args)PYpython3 ~/py/customdata.pymessage no such key: alphae.key alphae.args ('no such key: alpha',)Expected result
no such key: alpha, thene.keyisalpha, andargsholds the message.Success conditionYou can attach structured data to an exception rather than only text.
-
What super().__init__ actually controls
The usual claim is that omitting
super().__init__leavesargsandstr(e)empty. It does not -BaseExceptionstores the constructor arguments inargswhatever your__init__does.Three classes below: one that calls
super().__init__, one that does not, and one that does not and takes two arguments.bash Example session cat > ~/py/nosuper.py <<'PY'class WithSuper(Exception): def __init__(self, key): super().__init__("no such key: " + key) self.key = key class NoSuper(Exception): def __init__(self, key): self.key = key class TwoArgsNoSuper(Exception): def __init__(self, key, code): self.key = key self.code = code for cls, args in ((WithSuper, ("alpha",)), (NoSuper, ("alpha",)), (TwoArgsNoSuper, ("alpha", 42))): try: raise cls(*args) except Exception as e: print(cls.__name__.ljust(15), "str()", repr(str(e)), "| args", e.args)PYpython3 ~/py/nosuper.pyWithSuper str() 'no such key: alpha' | args ('no such key: alpha',)NoSuper str() 'alpha' | args ('alpha',)TwoArgsNoSuper str() "('alpha', 42)" | args ('alpha', 42)Expected result
WithSupershows the built message;NoSupershows'alpha';TwoArgsNoSupershows"('alpha', 42)".Success conditionYou can say what
str(e)will be for a custom exception before running it. -
What cannot be raised
An exception class must derive from
BaseException. An ordinary class cannot be raised however exception-shaped it looks.bash Example session cat > ~/py/notanexception.py <<'PY'class Bad: pass raise Bad()PYpython3 ~/py/notanexception.pyTraceback (most recent call last): File "/home/sysadmin/py/notanexception.py", line 5, in <module> raise Bad()TypeError: exceptions must derive from BaseException[exit 1]Expected result
TypeError: exceptions must derive from BaseException.Success conditionYou can name the error a non-exception class produces.
-
An uncaught custom exception
It looks exactly like a built-in one, with your class name in the last line. That is the whole benefit at the point where it matters most.
bash Example session cat > ~/py/uncaught.py <<'PY'class ConfigError(Exception): pass def load(path): raise ConfigError("cannot read " + path) load("/etc/app.conf")PYpython3 ~/py/uncaught.pyTraceback (most recent call last): File "/home/sysadmin/py/uncaught.py", line 9, in <module> load("/etc/app.conf") ~~~~^^^^^^^^^^^^^^^^^ File "/home/sysadmin/py/uncaught.py", line 6, in load raise ConfigError("cannot read " + path)ConfigError: cannot read /etc/app.conf[exit 1]Expected resultA normal traceback ending
ConfigError: cannot read /etc/app.conf.Success conditionYou can read a traceback from your own exception class.
-
What the exam does with this objective
Objective 2.2 is the smaller half of PCAP's 14% exceptions section, and its questions are narrow:
"What must a custom exception inherit from?" -
Exception, or something derived fromBaseException."Will
except AppError:catch aNotFound?" - yes, ifNotFoundderives from it."What does
raise Bad()do whenBadhas no base?" -TypeError."How do you attach extra data?" -
__init__,super().__init__(message), thenself.whatever = ...."What is the minimum body for a custom exception?" -
pass.That completes PCAP section 2. guide 4 starts section 1, modules and packages.
bash rm -f ~/py/customsimple.py ~/py/customfamily.py ~/py/customdata.py ~/py/nosuper.py ~/py/notanexception.py ~/py/uncaught.py && ls -A ~/pyExpected resultAn empty scratch directory.
Success conditionYou can extend the hierarchy deliberately rather than reusing
Exceptionfor everything.
Troubleshooting
TypeError: exceptions must derive from BaseException.Why: The class being raised does not inherit from an exception class.
Fix:
class MyError(Exception):. The error appears at theraise, not at the class definition.A custom exception's message prints as a tuple with brackets.
Why:
__init__takes several arguments and does not callsuper().__init__with a single built string.Fix:Build the message first, then
super().__init__(message), then store the individual values onself.except MyError:does not catch a subclass you expected it to.Why: The subclass does not actually inherit from
MyError.Fix:Check with
issubclass(Sub, MyError). A common cause is deriving fromExceptiondirectly out of habit.A custom exception loses its extra attribute after being re-raised.
Why:
raise MyError(str(e))creates a new object rather than passing the old one on.Fix:Use a bare
raiseto re-raise the same object, orraise NewError(...) from eto chain. See guide 1.