CertGrid CertGrid
Concepts·Certified Associate Python Programmer

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

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.

One machine, and any shell with Python 3 will do - these exams test the language, not a distribution.
Server NameIP AddressOSRolesCPURAMHDD
RUNNER01192.168.0.27Ubuntu 26.04 LTSPython 3.14.4 - the only machine this path needs2 Core4 GB50 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.

Before you start

  1. The smallest useful custom exception

    Three lines: a class, inheriting from Exception, with pass as 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: True

    Expected resultAppError | something went wrong in the application | args ('something went wrong in the application',), then True.

    Success conditionYou can define and raise an exception of your own.

  2. 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: gamma

    Expected resultAll three - NotFound, Forbidden and AppError itself - caught by the one except AppError: clause.

    Success conditionYou can design an exception family a caller can use at either level.

  3. Carrying extra data

    An exception is an object, so it can hold anything useful. Define __init__, call super().__init__ with the message, and store the rest on self.

    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 resultno such key: alpha, then e.key is alpha, and args holds the message.

    Success conditionYou can attach structured data to an exception rather than only text.

  4. What super().__init__ actually controls

    The usual claim is that omitting super().__init__ leaves args and str(e) empty. It does not - BaseException stores the constructor arguments in args whatever 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 resultWithSuper shows the built message; NoSuper shows 'alpha'; TwoArgsNoSuper shows "('alpha', 42)".

    Success conditionYou can say what str(e) will be for a custom exception before running it.

  5. 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 resultTypeError: exceptions must derive from BaseException.

    Success conditionYou can name the error a non-exception class produces.

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

  7. 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 from BaseException.

    "Will except AppError: catch a NotFound?" - yes, if NotFound derives from it.

    "What does raise Bad() do when Bad has no base?" - TypeError.

    "How do you attach extra data?" - __init__, super().__init__(message), then self.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 ~/py

    Expected resultAn empty scratch directory.

    Success conditionYou can extend the hierarchy deliberately rather than reusing Exception for everything.

Troubleshooting

Official sources