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
- Python3.14.4
- OSUbuntu 26.04 LTS
- pip25.1.1
- TimeAbout 15 min
- Reviewed23 August 2026
Written against the versions above. 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 |
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.