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

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.

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

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