LESSON 50 · DEEPER PYTHON29 min

Create a Custom Python Exception for Validation

Raise a meaningful exception type for invalid input while preserving the underlying conversion cause.

Before you start: Exceptions · Classes and Objects · Functions

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Create a Custom Python Exception for Validation

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Validation is not complete just because a string can be converted to an integer. Our order example requires one or more units. The text "two" cannot be converted, while the text "0" converts perfectly and still violates the business rule. If we write only return int(text), those inputs follow two inconsistent paths: a raw ValueError for the word, and a successful zero for the invalid quantity. We will build a small domain exception called QuantityError, translate the expected conversion failure without losing its cause, and apply the positive-quantity check after conversion. The goal is not to invent an exception for every function.

  2. 00:42

    A specific type is useful when a caller needs to respond to one domain failure while leaving unrelated programming bugs visible. In the companion browser lesson, you can repair the starter function and run a checker that inspects both the returned values and the exception path. Three is valid. Zero is not valid under this order rule, yet it returns as an ordinary result. The word "two" raises ValueError because int cannot parse it. The raw error type does not tell the caller that the problem concerns a quantity field, and the zero case does not raise anything at all. We need to separate two checks.

  3. 01:22

    First, is the text a decimal integer? Second, does that integer satisfy the one-or-more-units rule? A broad except around the whole function would be the wrong shortcut. It could catch a bug in unrelated code and report it as a quantity mistake. We should catch only the conversion error we expect and add an explicit rule afterward. The custom exception can inherit from ValueError so generic error handling still recognizes it as a bad value, while quantity-specific handling can catch it directly. The outer exception is now QuantityError, a subclass of ValueError. Its message names the invalid units rather than leaving the caller to infer the field from a generic conversion message.

  4. 02:08

    The original ValueError remains in __cause__ because the raise statement used from exc. If this error were unhandled, Python would show both exceptions in the traceback and say that the first was the direct cause of the second. We are not hiding the lower-level explanation; we are adding context at the boundary where the text becomes a domain quantity. Notice that a converted zero has no conversion exception to chain. It will need a direct QuantityError after the int call. That distinction makes the code easier to reason about and test. The caller can catch QuantityError to ask for a corrected number while a NameError or unexpected TypeError still surfaces as a separate problem.

  5. 02:52

    Conversion is isolated in the try block. Only its ValueError is translated, so a programming mistake elsewhere is not silently renamed. After conversion, the second if statement checks the rule that a quantity must be positive. Zero and negative values raise QuantityError directly, with the offending input in the message. A valid value returns as an integer. In the website task, the starter code only converts, so both failure paths need your attention. Use a small subclass rather than storing extra fields unless your caller actually needs them. The subclass adds a stable type, while the message adds useful human context.

  6. 03:34

    The exact message wording is a design choice; the checker only requires that the offending text be included. For text that cannot be converted, it also verifies that the original ValueError is preserved as the cause. A downstream caller could inspect __cause__ when logging a detailed diagnostic, while showing only the concise QuantityError message to the person entering an order. Those two audiences need different levels of detail. Keeping both exceptions available avoids choosing between a readable interface and a useful debugging trail. This encourages a correction that stays transparent during debugging.

  7. 04:15

    The first assertion shows that a valid multi-digit value can include surrounding whitespace and still convert to twelve. The loop probes zero and a negative value, two cases that conversion alone would accept. The final branch checks nonnumeric text and inspects the cause, so a solution that merely replaces the error message without chaining will not pass. These checks describe the function's contract better than a single successful print. Imagine a command-line inventory tool calling parse_units. It might catch QuantityError, show the specific message, and ask the person to enter the quantity again. A different error, such as an undefined variable in the reservation code, should still stop the program or reach its normal error reporter.

  8. 05:03

    A bare except would confuse those two situations and could make a damaged order look like bad input. The custom type creates a deliberate boundary between expected correction and a software defect. In a real system, you might also decide whether whitespace-only text, very large quantities, or signs such as plus should be accepted. Those are domain decisions, not properties a custom exception can determine for you. State the rule, validate it at the boundary, and keep unexpected bugs separate from expected bad input. Then run the companion task and quiz. If a different module calls parse_units, it can handle QuantityError intentionally while still retaining the original conversion failure for diagnostics when necessary.

Understand the concept

An exception is an object that describes a failure path. Python already provides ValueError when a value has the wrong content, such as int('two'). A custom subclass can make a domain rule easier for callers to catch specifically. For inventory quantities, class QuantityError(ValueError) marks a bad quantity without discarding the normal meaning of ValueError. Keep the subclass small unless it needs additional structured information.

Validation belongs at the boundary where the program first learns what its input means. Convert numeric text with int, then check the business rule: quantity must be positive. Zero may be valid for some domains, but in this lesson the contract explicitly requires one or more units. State that rule in the function name and message. A caller can catch QuantityError to ask for a corrected value while allowing unrelated bugs to surface.

When conversion raises ValueError, re-raise QuantityError with raise ... from exc. That preserves the original failure in __cause__ and shows both steps in a traceback. Without chaining, the custom message may be clearer but the cause is harder to inspect. Do not use a bare except: it would also catch unrelated programming errors and mislabel them as bad user input.

The practice function parse_units(text) accepts decimal integer text and returns a positive integer. It raises QuantityError for nonnumeric text and for zero or negative values. The checker inspects the subclass relationship, verifies a conversion cause for invalid text, and ensures a valid value is returned unchanged. It also checks that the error message contains the invalid input.

  • Custom exceptions can subclass a built-in error
  • Raise at the domain boundary
  • raise ... from exc preserves a cause
  • Catch only expected conversion errors

See it step by step

Read the code, predict the output, then compare it with the result.

01. Catch the domain exception only

PYTHON
class QuantityError(ValueError):
    pass

def reserve(units):
    if units < 1:
        raise QuantityError(f'Invalid units: {units}')
    return f'reserved {units}'

try:
    print(reserve(0))
except QuantityError as exc:
    print(type(exc).__name__, str(exc))
EXPECTED OUTPUT
QuantityError Invalid units: 0

The caller handles the domain failure without catching every unrelated exception.

02. Keep the conversion cause

PYTHON
class QuantityError(ValueError):
    pass

try:
    try:
        int('two')
    except ValueError as exc:
        raise QuantityError('Invalid units: two') from exc
except QuantityError as exc:
    print(type(exc.__cause__).__name__)
    print(str(exc))
EXPECTED OUTPUT
ValueError
Invalid units: two

The outer exception has a readable domain message while __cause__ preserves the original conversion error.

03. Separate conversion from a domain rule

PYTHON
def positive_units(text):
    units = int(text)
    if units <= 0:
        raise ValueError('units must be positive')
    return units

print(positive_units('4'))
try:
    positive_units('0')
except ValueError as exc:
    print(str(exc))
EXPECTED OUTPUT
4
units must be positive

Converting text and enforcing the domain rule are two distinct checks.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Give a domain failure its own type

Python's built-in ValueError communicates that a value has unacceptable content. An inventory application may benefit from a more specific QuantityError so its caller can offer a quantity-specific correction. A small subclass of ValueError keeps that general meaning while adding the domain signal. The exception type alone does not enforce a rule; the function must raise it at the right boundary.

The reserve function below rejects zero and negative quantities but accepts positive values. This requirement is local to the inventory example; another domain might legitimately accept zero. The caller catches QuantityError rather than every possible exception, so a separate programming fault will not be mislabeled as a user quantity mistake.

PYTHON
class QuantityError(ValueError):
    pass

def reserve(units):
    if units < 1:
        raise QuantityError(f'Invalid units: {units}')
    return f'reserved {units}'

try:
    print(reserve(0))
except QuantityError as exc:
    print(type(exc).__name__, str(exc))
print(reserve(3))
EXPECTED OUTPUT
QuantityError Invalid units: 0
reserved 3
Follow the reasoning
  1. The subclass names this domain failure.
  2. The invalid zero raises before a reservation is returned.
  3. The positive quantity follows the ordinary success path.
02 / 03

Preserve a lower-level conversion error

When the function receives text, conversion can fail before the domain rule is checked. int('many') raises ValueError. The function can catch that expected conversion failure and raise QuantityError with a message that refers to the input field. Adding from exc records the original ValueError as the explicit cause of the new exception.

The example inspects __cause__ rather than dumping the full traceback, so its output is stable and concise. In an unhandled run, Python would display both exceptions and say the first was the direct cause of the second. Do not place all business logic under a bare except; catch only conversion failures you intend to translate.

PYTHON
class QuantityError(ValueError):
    pass

def parse_units(text):
    try:
        return int(text)
    except ValueError as exc:
        raise QuantityError(f'Invalid units: {text!r}') from exc

try:
    parse_units('many')
except QuantityError as exc:
    print(str(exc))
    print(type(exc.__cause__).__name__)
EXPECTED OUTPUT
Invalid units: 'many'
ValueError
Follow the reasoning
  1. int fails on the nonnumeric text.
  2. The function translates that expected failure to QuantityError.
  3. The original ValueError remains available as __cause__.
03 / 03

Validate the converted value too

Successful conversion is not the same as a valid quantity. int('0') returns zero, so the function must separately check the positive-units rule. It raises the same domain exception for a nonpositive value, with the offending text in the message. A valid decimal integer string, including surrounding whitespace that int accepts, returns its numeric value.

The practice checker exercises positive, zero, negative, and nonnumeric text. It also verifies that invalid text preserves a conversion cause. A solution that only catches int errors misses the domain rule; a solution that only checks the resulting number leaves raw conversion errors. Both steps are needed for the complete contract.

PYTHON
class QuantityError(ValueError):
    pass

def parse_units(text):
    try:
        units = int(text)
    except ValueError as exc:
        raise QuantityError(f'Invalid units: {text!r}') from exc
    if units <= 0:
        raise QuantityError(f'Invalid units: {text!r}')
    return units

print(parse_units(' 12 '))
try:
    parse_units('0')
except QuantityError as exc:
    print(str(exc))
EXPECTED OUTPUT
12
Invalid units: '0'
Follow the reasoning
  1. Conversion first establishes an integer.
  2. The domain rule rejects nonpositive values.
  3. A valid multi-digit value returns unchanged.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete parse_units(text). Return an integer only when it is at least one. For invalid integer text or nonpositive values raise QuantityError with the offending text in the message. For conversion failures use raise QuantityError(...) from exc. The prepared call should print 3.
Need a hint?

Wrap only int(text) in try/except ValueError as exc. Re-raise QuantityError with from exc. After conversion, reject units <= 0 with another QuantityError.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why subclass ValueError for QuantityError?

    02.What does raise QuantityError(...) from exc record?

    03.Why avoid bare except in input validation?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Raising the generic conversion error only

    COMMON MISTAKE
    return int(text)
    FIX
    try:
        units = int(text)
    except ValueError as exc:
        raise QuantityError(f'Invalid units: {text!r}') from exc
    if units <= 0:
        raise QuantityError(f'Invalid units: {text!r}')
    return units

    What happens: Nonnumeric text raises ValueError, while zero and negatives pass through.
    Conversion and the positive-quantity rule both belong to the function's contract.

    Swallowing every exception

    COMMON MISTAKE
    try:
        return calculate(text)
    except:
        return 0
    FIX
    except ValueError as exc:
        raise QuantityError(f'Invalid units: {text!r}') from exc

    What happens: A bug inside calculate is silently converted into a valid-looking zero.
    Catch the expected failure and keep a diagnosable exception path.

    Browse this lesson’s problem fixes

    Sources and further reading