Skip to main content

Python Raising Exceptions Practice Problems & Exercises

Practice: Raising Exceptions

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Basic Raise with ValueErrorEasy
raiseValueErrorbasic

Write a function that validates an age value. It should raise TypeError when the input is not an integer, and ValueError when the integer is outside the range 0-150. Return the age unchanged if valid.

This tests the most fundamental raise pattern: checking a condition and raising with a descriptive message.

Python
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError(
            "age must be int, got " + repr(type(age).__name__)
        )
    if age < 0 or age > 150:
        raise ValueError(
            "age must be between 0 and 150, got " + str(age)
        )
    return age

print(validate_age(25))

try:
    validate_age("twenty")
except TypeError as e:
    print("Caught TypeError:", e)

try:
    validate_age(-5)
except ValueError as e:
    print("Caught ValueError:", e)

try:
    validate_age(200)
except ValueError as e:
    print("Caught ValueError:", e)
Solution
def validate_age(age):
if not isinstance(age, int):
raise TypeError(
"age must be int, got " + repr(type(age).__name__)
)
if age < 0 or age > 150:
raise ValueError(
"age must be between 0 and 150, got " + str(age)
)
return age

The raise statement creates an exception instance and immediately unwinds the call stack. TypeError signals that the caller passed the wrong type; ValueError signals the right type but an invalid value. Including the actual bad value in the message (got -5) tells the caller exactly what went wrong without needing to attach a debugger. This is Form 1 of raise: create and raise in one step.

def validate_age(age):
    """Validate that age is a non-negative integer.
    Raise TypeError if age is not an int.
    Raise ValueError if age is negative or greater than 150.
    Return the age if valid.
    """
    # TODO: implement
    pass
Expected Output
25
Caught TypeError: age must be int, got 'str'
Caught ValueError: age must be between 0 and 150, got -5
Caught ValueError: age must be between 0 and 150, got 200
Hints

Hint 1: Use isinstance(age, int) to check the type. Remember that bool is a subclass of int, but for this exercise you can ignore that.

Hint 2: Raise TypeError for wrong types and ValueError for out-of-range values. Include the bad value in the message.

#2Bare Re-raise After LoggingEasy
bare-raisere-raiselogging

Write a function that divides two numbers. If a ZeroDivisionError occurs, log the error message to a list, then re-raise the original exception using bare raise. The bare raise preserves the original traceback.

Test with (10, 2) for the happy path, then (10, 0) to trigger the error.

Python
def safe_divide(a, b, log):
    try:
        return a / b
    except ZeroDivisionError as e:
        log.append(str(e))
        raise

error_log = []
print(safe_divide(10, 2, error_log))

try:
    safe_divide(10, 0, error_log)
except ZeroDivisionError as e:
    print("Caught:", e)

print("Log:", error_log)
Solution
def safe_divide(a, b, log):
try:
return a / b
except ZeroDivisionError as e:
log.append(str(e))
raise

Bare raise re-raises the current exception with its original traceback intact. This is the correct way to log-and-propagate: the caller sees the exception as if it was never caught, with the traceback pointing to the original a / b line. If you wrote raise e instead, Python would create a new traceback starting at the raise e line, losing the original location. Always use bare raise when re-raising.

def safe_divide(a, b, log):
    """Divide a by b.
    If ZeroDivisionError occurs, append the error message
    string to the log list, then re-raise the ORIGINAL
    exception using bare raise.
    Return the result if no error.
    """
    # TODO: implement
    pass
Expected Output
5.0
Caught: division by zero
Log: ['division by zero']
Hints

Hint 1: Wrap the division in try/except ZeroDivisionError.

Hint 2: Inside the except block, append str(e) to the log list. Then use bare raise (just the word raise, no argument) to re-raise.

#3Pre-created Exception InstanceEasy
raisepre-createdForm-2

Write an email validator that builds exception instances as separate variables before raising them. This tests Form 2 of raise: pre-creating the exception object when the message requires complex logic.

Test with "[email protected]", 42, "alice", and "@example.com".

Python
def validate_email(email):
    if not isinstance(email, str):
        exc = TypeError(
            "email must be str, got " + repr(type(email).__name__)
        )
        raise exc
    at_count = email.count("@")
    if at_count != 1:
        exc = ValueError(
            "email must contain exactly one '@', got "
            + str(at_count) + " in " + repr(email)
        )
        raise exc
    local, domain = email.split("@")
    if not local or not domain:
        exc = ValueError(
            "email must have non-empty local and domain parts, got "
            + repr(email)
        )
        raise exc
    return email

print(validate_email("[email protected]"))

try:
    validate_email(42)
except TypeError as e:
    print("Caught TypeError:", e)

try:
    validate_email("alice")
except ValueError as e:
    print("Caught ValueError:", e)

try:
    validate_email("@example.com")
except ValueError as e:
    print("Caught ValueError:", e)
Solution
def validate_email(email):
if not isinstance(email, str):
exc = TypeError(
"email must be str, got " + repr(type(email).__name__)
)
raise exc
at_count = email.count("@")
if at_count != 1:
exc = ValueError(
"email must contain exactly one '@', got "
+ str(at_count) + " in " + repr(email)
)
raise exc
local, domain = email.split("@")
if not local or not domain:
exc = ValueError(
"email must have non-empty local and domain parts, got "
+ repr(email)
)
raise exc
return email

Form 2 of raise separates exception construction from raising. Writing exc = ValueError(...) then raise exc is functionally identical to raise ValueError(...), but it is cleaner when the message requires multi-line string building or conditional logic. The exception object exists as a regular Python object before raise transfers it to the exception handling machinery.

def validate_email(email):
    """Validate a simple email string.
    Rules:
    - Must be a string (raise TypeError)
    - Must contain exactly one '@' (raise ValueError)
    - Must have non-empty parts before and after '@' (raise ValueError)
    Build the ValueError with a detailed message BEFORE raising it.
    """
    # TODO: implement
    pass
Expected Output
[email protected]
Caught TypeError: email must be str, got 'int'
Caught ValueError: email must contain exactly one '@', got 0 in 'alice'
Caught ValueError: email must have non-empty local and domain parts, got '@example.com'
Hints

Hint 1: Count the "@" characters with email.count("@"). If not exactly 1, build and raise a ValueError.

Hint 2: Split on "@" and check that both parts are non-empty strings. Build the exception object first, assign to a variable, then raise it.

#4Guard Clauses at Function BoundariesEasy
guard-clausefail-fastvalidation

Write a function with guard clauses that validate both parameters before doing any work. The guards should check types first, then values. Only after all guards pass should the function compute the result.

This tests the "fail fast" principle: catch invalid inputs at the boundary.

Python
def create_rectangle(width, height):
    if not isinstance(width, (int, float)):
        raise TypeError(
            "width must be numeric, got " + repr(type(width).__name__)
        )
    if not isinstance(height, (int, float)):
        raise TypeError(
            "height must be numeric, got " + repr(type(height).__name__)
        )
    if width <= 0:
        raise ValueError("width must be positive, got " + str(width))
    if height <= 0:
        raise ValueError("height must be positive, got " + str(height))
    return {"width": width, "height": height, "area": width * height}

print(create_rectangle(5, 3))

try:
    create_rectangle("five", 3)
except TypeError as e:
    print("Caught TypeError:", e)

try:
    create_rectangle(5, -2)
except ValueError as e:
    print("Caught ValueError:", e)

try:
    create_rectangle(0, 3)
except ValueError as e:
    print("Caught ValueError:", e)
Solution
def create_rectangle(width, height):
if not isinstance(width, (int, float)):
raise TypeError(
"width must be numeric, got " + repr(type(width).__name__)
)
if not isinstance(height, (int, float)):
raise TypeError(
"height must be numeric, got " + repr(type(height).__name__)
)
if width <= 0:
raise ValueError("width must be positive, got " + str(width))
if height <= 0:
raise ValueError("height must be positive, got " + str(height))
return {"width": width, "height": height, "area": width * height}

Guard clauses validate inputs at the very top of a function, before any real work begins. Each guard is a simple if bad: raise pattern. The order matters: check types first (TypeError), then check values (ValueError). After all guards pass, the function body runs with guaranteed-valid inputs. This eliminates the need for defensive checks deeper in the code and makes the function's contract explicit.

def create_rectangle(width, height):
    """Create a rectangle dict with width, height, and area.
    Guard clauses:
    - width must be a positive number (int or float)
    - height must be a positive number (int or float)
    Raise TypeError for non-numeric, ValueError for non-positive.
    Return dict with keys: 'width', 'height', 'area'
    """
    # TODO: implement with guard clauses at the top
    pass
Expected Output
{'width': 5, 'height': 3, 'area': 15}
Caught TypeError: width must be numeric, got 'str'
Caught ValueError: height must be positive, got -2
Caught ValueError: width must be positive, got 0
Hints

Hint 1: Put all validation checks at the very top of the function, before any computation.

Hint 2: Check isinstance(width, (int, float)) for type, then width <= 0 for value. Same for height. After all guards pass, compute and return the dict.


Medium

#5Explicit Exception Chaining with raise...fromMedium
raise-fromexception-chaining__cause__

Write a config loader that translates low-level json.JSONDecodeError into a domain-specific ConfigError using explicit exception chaining (raise ... from e). When the JSON is valid but not a dict, raise ConfigError without chaining.

Check __cause__ to verify chaining works correctly.

Python
import json

class ConfigError(Exception):
    pass

def load_config(raw_json):
    try:
        result = json.loads(raw_json)
    except json.JSONDecodeError as e:
        raise ConfigError(
            "Invalid config format: " + str(e)
        ) from e
    if not isinstance(result, dict):
        raise ConfigError(
            "Config must be a JSON object, got "
            + type(result).__name__
        )
    return result

print(load_config('{"host": "localhost", "port": 8080}'))

try:
    load_config("not json at all")
except ConfigError as e:
    print("Caught ConfigError:", e)
    print("Has __cause__:", e.__cause__ is not None)
    print("Original error type:", type(e.__cause__).__name__)

try:
    load_config('[1, 2, 3]')
except ConfigError as e:
    print("Caught ConfigError:", e)
    print("Has __cause__:", e.__cause__ is not None)
Solution
import json

class ConfigError(Exception):
pass

def load_config(raw_json):
try:
result = json.loads(raw_json)
except json.JSONDecodeError as e:
raise ConfigError(
"Invalid config format: " + str(e)
) from e
if not isinstance(result, dict):
raise ConfigError(
"Config must be a JSON object, got "
+ type(result).__name__
)
return result

raise X from Y sets X.__cause__ = Y and produces the traceback message "The above exception was the direct cause of the following exception." This is explicit chaining: the caller sees both the high-level domain error (ConfigError) and the root technical cause (JSONDecodeError). When there is no underlying exception to chain from (the not a dict case), you raise without from and __cause__ is None. Use explicit chaining in library code whenever you translate a low-level exception into a domain-specific one.

class ConfigError(Exception):
    """Domain-specific configuration error."""
    pass

def load_config(raw_json):
    """Parse a JSON config string.
    If JSON parsing fails, raise ConfigError chained
    from the original json.JSONDecodeError.
    If the parsed result is not a dict, raise ConfigError
    (no chaining needed).
    Return the parsed config dict.
    """
    import json
    # TODO: implement
    pass
Expected Output
{'host': 'localhost', 'port': 8080}
Caught ConfigError: Invalid config format: Expecting value: line 1 column 1 (char 0)
Has __cause__: True
Original error type: JSONDecodeError
Caught ConfigError: Config must be a JSON object, got list
Has __cause__: False
Hints

Hint 1: Wrap json.loads() in a try/except json.JSONDecodeError. In the except block, raise ConfigError(...) from e.

Hint 2: After successful parsing, check isinstance(result, dict). If not, raise ConfigError without chaining since there is no underlying exception to chain from.

#6Suppressing the Chain with raise...from NoneMedium
raise-from-Nonesuppress-contextencapsulation

Build an Inventory class that hides its internal dict implementation from callers. When a lookup fails, the internal KeyError should be suppressed with from None so callers only see the domain-specific ItemNotFoundError.

Verify that __cause__ is None and __suppress_context__ is True.

Python
class ItemNotFoundError(Exception):
    pass

class Inventory:
    def __init__(self):
        self._items = {}

    def add(self, name, quantity):
        self._items[name] = quantity

    def get_quantity(self, name):
        try:
            return self._items[name]
        except KeyError:
            raise ItemNotFoundError(
                "Item " + repr(name) + " not found in inventory"
            ) from None

    def remove(self, name, amount):
        current = self.get_quantity(name)
        if amount > current:
            raise ValueError(
                "Cannot remove " + str(amount) + " units of "
                + repr(name) + " (only " + str(current)
                + " available)"
            )
        self._items[name] = current - amount

inv = Inventory()
inv.add("apple", 50)
print(inv.get_quantity("apple"))

try:
    inv.get_quantity("banana")
except ItemNotFoundError as e:
    print("Caught:", e)
    print("__cause__ is None:", e.__cause__ is None)
    print("__suppress_context__:", e.__suppress_context__)

try:
    inv.remove("apple", 200)
except ValueError as e:
    print("Caught:", e)
Solution
class ItemNotFoundError(Exception):
pass

class Inventory:
def __init__(self):
self._items = {}

def add(self, name, quantity):
self._items[name] = quantity

def get_quantity(self, name):
try:
return self._items[name]
except KeyError:
raise ItemNotFoundError(
"Item " + repr(name) + " not found in inventory"
) from None

def remove(self, name, amount):
current = self.get_quantity(name)
if amount > current:
raise ValueError(
"Cannot remove " + str(amount) + " units of "
+ repr(name) + " (only " + str(current)
+ " available)"
)
self._items[name] = current - amount

raise X from None sets __cause__ = None and __suppress_context__ = True, hiding the internal exception entirely. Without from None, Python would show "During handling of the above exception, another exception occurred:" followed by the KeyError, leaking the fact that your class uses a dict internally. With from None, the caller only sees ItemNotFoundError. This is proper encapsulation: if you later switch from a dict to a database, the public interface stays the same.

class ItemNotFoundError(Exception):
    """Raised when a lookup fails in the inventory."""
    pass

class Inventory:
    def __init__(self):
        self._items = {}

    def add(self, name, quantity):
        self._items[name] = quantity

    def get_quantity(self, name):
        """Return the quantity of an item.
        If item not found, raise ItemNotFoundError.
        Suppress the internal KeyError — it is an
        implementation detail.
        """
        # TODO: implement
        pass

    def remove(self, name, amount):
        """Remove amount units of an item.
        Raise ItemNotFoundError if item missing.
        Raise ValueError if removing more than available.
        Suppress internal KeyError.
        """
        # TODO: implement
        pass
Expected Output
50
Caught: Item 'banana' not found in inventory
__cause__ is None: True
__suppress_context__: True
Caught: Cannot remove 200 units of 'apple' (only 50 available)
Hints

Hint 1: In get_quantity, wrap self._items[name] in try/except KeyError. In the except block, raise ItemNotFoundError(...) from None.

Hint 2: In remove, call get_quantity first (it handles the not-found case). Then check if amount exceeds the available quantity and raise ValueError if so.

#7When to Raise vs When to Return NoneMedium
raise-vs-returnAPI-designfind-vs-get

Implement two lookup functions demonstrating the find vs get naming convention. find_student returns None for missing entries (not-found is expected). get_student raises LookupError for missing entries (not-found is an error) and ValueError for invalid input.

This tests the design decision of when to raise vs when to return a sentinel.

Python
def find_student(students, student_id):
    for s in students:
        if s["id"] == student_id:
            return s
    return None

def get_student(students, student_id):
    if not isinstance(student_id, int) or student_id <= 0:
        raise ValueError(
            "student_id must be a positive int, got "
            + str(student_id)
        )
    for s in students:
        if s["id"] == student_id:
            return s
    raise LookupError(
        "No student found with id " + str(student_id)
    )

students = [
    {"id": 1, "name": "Alice", "grade": "A"},
    {"id": 2, "name": "Bob", "grade": "B"},
]

print("find Alice:", find_student(students, 1))
print("find unknown:", find_student(students, 99))
print("get Bob:", get_student(students, 2))

try:
    get_student(students, -1)
except ValueError as e:
    print("get invalid: Caught ValueError:", e)

try:
    get_student(students, 99)
except LookupError as e:
    print("get missing: Caught LookupError:", e)
Solution
def find_student(students, student_id):
for s in students:
if s["id"] == student_id:
return s
return None

def get_student(students, student_id):
if not isinstance(student_id, int) or student_id <= 0:
raise ValueError(
"student_id must be a positive int, got "
+ str(student_id)
)
for s in students:
if s["id"] == student_id:
return s
raise LookupError(
"No student found with id " + str(student_id)
)

The find vs get convention communicates the contract through naming. find_X returns None when "not found" is a normal, expected outcome that callers handle routinely. get_X raises when "not found" means something is wrong. Providing both lets callers choose the behavior they need. get_student also validates input with a guard clause because invalid IDs are programmer errors, not normal outcomes. The key design question is: "Is not-found a normal case or an error?" The answer depends on the caller's context.

def find_student(students, student_id):
    """Search for a student by ID.
    Return the student dict if found, None if not found.
    This is the 'find' pattern — not-found is normal.
    """
    # TODO: implement
    pass

def get_student(students, student_id):
    """Get a student by ID.
    Return the student dict if found.
    Raise ValueError if student_id is not a positive int.
    Raise LookupError if no student with that ID exists.
    This is the 'get' pattern — not-found is an error.
    """
    # TODO: implement
    pass
Expected Output
find Alice: {'id': 1, 'name': 'Alice', 'grade': 'A'}
find unknown: None
get Bob: {'id': 2, 'name': 'Bob', 'grade': 'B'}
get invalid: Caught ValueError: student_id must be a positive int, got -1
get missing: Caught LookupError: No student found with id 99
Hints

Hint 1: find_student simply iterates and returns the match, or returns None at the end. No exceptions.

Hint 2: get_student validates the input first (guard clause), then iterates. If the loop ends without finding the student, raise LookupError.

#8Good Error Messages with ContextMedium
error-messagesreprcontext

Write a hex color parser with exceptional error messages. Each message must include: (1) the bad value using repr, (2) what was expected, and (3) a hint for the most common mistake. This tests the art of writing error messages that help callers fix their code quickly.

Python
def parse_color_hex(value):
    if not isinstance(value, str):
        raise TypeError(
            "value must be str, got " + repr(type(value).__name__)
            + ". Did you mean '#FF0000' (a string)?"
        )
    if not value.startswith("#"):
        raise ValueError(
            "color must start with '#', got " + repr(value)
            + ". Did you forget the '#' prefix?"
        )
    if len(value) != 7:
        raise ValueError(
            "color must be 7 characters (#RRGGBB), got "
            + str(len(value)) + " characters in " + repr(value)
            + ". Did you use shorthand? Expand #RGB to #RRGGBB."
        )
    hex_part = value[1:]
    try:
        int(hex_part, 16)
    except ValueError:
        invalid = "".join(
            c for c in hex_part if c not in "0123456789ABCDEFabcdef"
        )
        raise ValueError(
            "color contains invalid hex characters: "
            + repr(invalid) + " in " + repr(value)
            + ". Valid hex digits are 0-9 and A-F."
        ) from None
    r = int(hex_part[0:2], 16)
    g = int(hex_part[2:4], 16)
    b = int(hex_part[4:6], 16)
    return (r, g, b)

print(parse_color_hex("#FF0000"))
print(parse_color_hex("#00FF00"))

for bad_input in [255, "FF0000", "#FFF", "#GG0000"]:
    try:
        parse_color_hex(bad_input)
    except (TypeError, ValueError) as e:
        print(type(e).__name__ + ":", e)
Solution
def parse_color_hex(value):
if not isinstance(value, str):
raise TypeError(
"value must be str, got " + repr(type(value).__name__)
+ ". Did you mean '#FF0000' (a string)?"
)
if not value.startswith("#"):
raise ValueError(
"color must start with '#', got " + repr(value)
+ ". Did you forget the '#' prefix?"
)
if len(value) != 7:
raise ValueError(
"color must be 7 characters (#RRGGBB), got "
+ str(len(value)) + " characters in " + repr(value)
+ ". Did you use shorthand? Expand #RGB to #RRGGBB."
)
hex_part = value[1:]
try:
int(hex_part, 16)
except ValueError:
invalid = "".join(
c for c in hex_part if c not in "0123456789ABCDEFabcdef"
)
raise ValueError(
"color contains invalid hex characters: "
+ repr(invalid) + " in " + repr(value)
+ ". Valid hex digits are 0-9 and A-F."
) from None
r = int(hex_part[0:2], 16)
g = int(hex_part[2:4], 16)
b = int(hex_part[4:6], 16)
return (r, g, b)

A good error message answers three questions: what went wrong, what was expected, and what the caller probably meant to do. Using repr() on the bad value is critical: it shows quotes around strings (distinguishing '' from nothing), shows the type of non-string values, and makes whitespace visible. The hint ("Did you forget the '#' prefix?") anticipates the most common mistake for each validation. Production libraries like pydantic and click follow this pattern. The few extra seconds writing a good message saves minutes of debugging for every caller who triggers the error.

def parse_color_hex(value):
    """Parse a hex color string like '#FF0000' into (r, g, b) tuple.
    Requirements for error messages:
    - Include the bad value using repr
    - Include what was expected
    - Include a hint for common mistakes
    
    Validations:
    - Must be a string (TypeError)
    - Must start with '#' (ValueError with hint)
    - Must be exactly 7 chars long (ValueError)
    - Must contain valid hex digits after '#' (ValueError)
    Return tuple of (r, g, b) as integers 0-255.
    """
    # TODO: implement
    pass
Expected Output
(255, 0, 0)
(0, 255, 0)
TypeError: value must be str, got 'int'. Did you mean '#FF0000' (a string)?
ValueError: color must start with '#', got 'FF0000'. Did you forget the '#' prefix?
ValueError: color must be 7 characters (#RRGGBB), got 4 characters in '#FFF'. Did you use shorthand? Expand #RGB to #RRGGBB.
ValueError: color contains invalid hex characters: 'GG' in '#GG0000'. Valid hex digits are 0-9 and A-F.
Hints

Hint 1: Use repr() or !r to show the bad value clearly, especially for empty strings or strings with whitespace.

Hint 2: For each validation, include three things: what went wrong, what was expected, and a hint for the most common mistake that would trigger this error.


Hard

#9Exception Translation LayerHard
raise-fromchaininglayer-boundaryarchitecture

Build a storage layer that translates all internal exceptions into domain-specific StorageError subclasses. Use from e when the original error has useful context, and from None when the original error is an implementation detail. This tests exception chaining decisions at architectural boundaries.

Python
class StorageError(Exception):
    pass

class RecordNotFoundError(StorageError):
    pass

class StorageCorruptionError(StorageError):
    pass

class InMemoryStore:
    def __init__(self):
        self._data = {}

    def save(self, key, record):
        if not isinstance(key, str) or not key:
            raise StorageError(
                "key must be a non-empty string, got " + repr(key)
            )
        if not isinstance(record, dict):
            raise StorageError(
                "record must be a dict, got "
                + type(record).__name__
            )
        self._data[key] = record

    def load(self, key):
        try:
            value = self._data[key]
        except KeyError:
            raise RecordNotFoundError(
                "No record with key " + repr(key)
            ) from None
        if not isinstance(value, dict):
            cause = TypeError(
                "expected dict, got " + type(value).__name__
            )
            raise StorageCorruptionError(
                "Record " + repr(key)
                + " is corrupted: expected dict, got "
                + type(value).__name__
            ) from cause
        return value

    def delete(self, key):
        try:
            del self._data[key]
        except KeyError:
            raise RecordNotFoundError(
                "No record with key " + repr(key)
            ) from None

store = InMemoryStore()
store.save("user1", {"name": "Alice", "role": "admin"})
print("Saved and loaded:", store.load("user1"))

try:
    store.load("unknown")
except RecordNotFoundError as e:
    print("RecordNotFoundError:", e)
    print("  __cause__ is None (suppressed):", e.__cause__ is None)

store._data["bad_key"] = "not a dict"
try:
    store.load("bad_key")
except StorageCorruptionError as e:
    print("StorageCorruptionError:", e)
    print("  Has __cause__:", e.__cause__ is not None)
    print("  __cause__ type:", type(e.__cause__).__name__)

store.delete("user1")
try:
    store.load("user1")
except RecordNotFoundError as e:
    print("RecordNotFoundError after delete:", e)

try:
    store.delete("user1")
except StorageError as e:
    print("Caught StorageError base class:", e)
Solution
class StorageError(Exception):
pass

class RecordNotFoundError(StorageError):
pass

class StorageCorruptionError(StorageError):
pass

class InMemoryStore:
def __init__(self):
self._data = {}

def save(self, key, record):
if not isinstance(key, str) or not key:
raise StorageError(
"key must be a non-empty string, got " + repr(key)
)
if not isinstance(record, dict):
raise StorageError(
"record must be a dict, got "
+ type(record).__name__
)
self._data[key] = record

def load(self, key):
try:
value = self._data[key]
except KeyError:
raise RecordNotFoundError(
"No record with key " + repr(key)
) from None
if not isinstance(value, dict):
cause = TypeError(
"expected dict, got " + type(value).__name__
)
raise StorageCorruptionError(
"Record " + repr(key)
+ " is corrupted: expected dict, got "
+ type(value).__name__
) from cause
return value

def delete(self, key):
try:
del self._data[key]
except KeyError:
raise RecordNotFoundError(
"No record with key " + repr(key)
) from None

Exception translation layers are the backbone of clean architecture. At every layer boundary (storage, service, API), internal exceptions should be translated into domain-specific ones. The decision of from e vs from None is architectural: use from e when the caller benefits from seeing the root cause (corruption errors), use from None when the internal exception is a pure implementation detail (KeyError from a dict). Callers can catch the base StorageError for generic handling or specific subclasses for targeted recovery. This is the same pattern used by SQLAlchemy, requests, and Django's ORM.

class StorageError(Exception):
    """Base error for the storage layer."""
    pass

class RecordNotFoundError(StorageError):
    """Raised when a record does not exist."""
    pass

class StorageCorruptionError(StorageError):
    """Raised when stored data is invalid."""
    pass

class InMemoryStore:
    """Simulates a storage backend with a dict.
    Translates all internal exceptions into StorageError
    subclasses using proper exception chaining.
    """
    def __init__(self):
        self._data = {}

    def save(self, key, record):
        """Save a record. Key must be a non-empty string.
        Record must be a dict.
        Raise TypeError with chaining if types are wrong.
        """
        # TODO: implement
        pass

    def load(self, key):
        """Load a record by key.
        Raise RecordNotFoundError (from None) if missing.
        Raise StorageCorruptionError (from original) if
        the stored value is not a dict.
        """
        # TODO: implement
        pass

    def delete(self, key):
        """Delete a record by key.
        Raise RecordNotFoundError (from None) if missing.
        """
        # TODO: implement
        pass
Expected Output
Saved and loaded: {'name': 'Alice', 'role': 'admin'}
RecordNotFoundError: No record with key 'unknown'
  __cause__ is None (suppressed): True
StorageCorruptionError: Record 'bad_key' is corrupted: expected dict, got str
  Has __cause__: True
  __cause__ type: TypeError
RecordNotFoundError after delete: No record with key 'user1'
Caught StorageError base class: No record with key 'user1'
Hints

Hint 1: In save(), validate types then store. In load(), catch KeyError and raise RecordNotFoundError from None. If the value is not a dict, raise StorageCorruptionError from TypeError.

Hint 2: In delete(), use dict.pop or try/except KeyError. RecordNotFoundError should suppress the chain since KeyError is an implementation detail.

#10Re-raise vs Wrap vs Suppress Decision EngineHard
re-raiseraise-fromraise-from-Nonedecision

Implement a multi-stage record processor where each stage uses a different exception handling strategy. This tests your ability to choose the right pattern for each situation: explicit chain, suppress chain, bare re-raise, or fresh raise.

Python
import json

class AppError(Exception):
    pass

def process_record(raw):
    # Stage 1: Parse JSON — chain from original
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        raise AppError(
            "Failed to parse record: " + str(e)
        ) from e

    # Stage 2: Validate fields — suppress internal KeyError
    try:
        name = data["name"]
        value = data["value"]
    except KeyError as e:
        raise AppError(
            "Record missing required field " + str(e)
        ) from None

    # Stage 3: Convert value — bare re-raise
    try:
        value = float(value)
    except ValueError:
        raise

    # Stage 4: Business rule — fresh raise
    if value <= 0:
        raise AppError(
            "Business rule violation: value must be positive, got "
            + str(value)
        )

    return {"name": name, "value": value}

# Happy path
print(process_record('{"name": "temperature", "value": 23.5}'))

# Stage 1: bad JSON
try:
    process_record("not json")
except AppError as e:
    print("Stage 1 - AppError:", e)
    print("  Chained (__cause__):", type(e.__cause__).__name__)

# Stage 2: missing field
try:
    process_record('{"name": "temp"}')
except AppError as e:
    print("Stage 2 - AppError:", e)
    print("  Suppressed (__cause__ is None):", e.__cause__ is None)

# Stage 3: bad value type
try:
    process_record('{"name": "temp", "value": "abc"}')
except ValueError as e:
    print("Stage 3 - ValueError:", e)
    print("  (bare re-raise, original error)")

# Stage 4: negative value
try:
    process_record('{"name": "temp", "value": -10}')
except AppError as e:
    print("Stage 4 - AppError:", e)
Solution
import json

class AppError(Exception):
pass

def process_record(raw):
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise AppError(
"Failed to parse record: " + str(e)
) from e

try:
name = data["name"]
value = data["value"]
except KeyError as e:
raise AppError(
"Record missing required field " + str(e)
) from None

try:
value = float(value)
except ValueError:
raise

if value <= 0:
raise AppError(
"Business rule violation: value must be positive, got "
+ str(value)
)

return {"name": name, "value": value}

Each stage uses a different strategy because each has different needs. Stage 1 chains (from e) because the JSON parse error details (line number, position) are valuable to the caller. Stage 2 suppresses (from None) because KeyError: 'value' is an implementation detail -- the caller does not care that you used dict access. Stage 3 bare re-raises because ValueError: could not convert string to float: 'abc' is already a perfect message that needs no translation. Stage 4 raises fresh because there is no underlying exception -- it is a business rule check. The decision framework: chain when the cause has useful context, suppress when it leaks internals, re-raise when the original is already clear, and raise fresh when detecting a new error.

import json

class AppError(Exception):
    pass

def process_record(raw):
    """Process a JSON record string through multiple stages.
    Each stage has different exception handling:
    
    Stage 1 - Parse JSON:
      On json.JSONDecodeError -> raise AppError FROM the original
      (caller needs to see parsing details)
    
    Stage 2 - Validate required fields ('name' and 'value'):
      On missing field -> raise AppError FROM NONE
      (KeyError is implementation detail)
    
    Stage 3 - Convert value to float:
      On ValueError -> BARE RE-RAISE
      (already a clear error, no translation needed)
    
    Stage 4 - Business rule: value must be positive:
      On violation -> raise NEW AppError (no chaining)
    
    Return dict with 'name' (str) and 'value' (float).
    """
    # TODO: implement all four stages
    pass
Expected Output
{'name': 'temperature', 'value': 23.5}
Stage 1 - AppError: Failed to parse record: Expecting value: line 1 column 1 (char 0)
  Chained (__cause__): JSONDecodeError
Stage 2 - AppError: Record missing required field 'value'
  Suppressed (__cause__ is None): True
Stage 3 - ValueError: could not convert string to float: 'abc'
  (bare re-raise, original error)
Stage 4 - AppError: Business rule violation: value must be positive, got -10.0
Hints

Hint 1: Stage 1: try json.loads, except JSONDecodeError as e, raise AppError(...) from e.

Hint 2: Stage 2: try accessing data["name"] and data["value"], except KeyError, raise AppError(...) from None.

Hint 3: Stage 3: try float(data["value"]), except ValueError, bare raise. Stage 4: if value <= 0, raise AppError (no from).

#11Validation Framework with Collected ErrorsHard
multiple-errorsvalidationraisearchitecture

Build a validation framework that collects all errors before raising a single ValidationError. Most validators stop at the first error, forcing users to fix one issue at a time. This pattern collects every problem so the caller can fix them all at once.

This tests raising with rich context and the architectural decision of when to raise immediately (type check) vs collect and raise (field validations).

Python
class ValidationError(Exception):
    def __init__(self, errors):
        self.errors = errors
        super().__init__(
            str(len(errors)) + " validation error(s)"
        )

def validate_user_registration(data):
    if not isinstance(data, dict):
        raise TypeError(
            "data must be a dict, got " + repr(type(data).__name__)
        )

    errors = []

    # Username
    if "username" not in data:
        errors.append("Missing required field: 'username'")
    else:
        u = data["username"]
        if not isinstance(u, str):
            errors.append(
                "username must be str, got " + type(u).__name__
            )
        elif len(u) < 3 or len(u) > 20:
            errors.append(
                "username must be 3-20 characters, got "
                + str(len(u))
            )
        elif not u.isalnum():
            errors.append(
                "username must be alphanumeric, got "
                + repr(u)
            )

    # Email
    if "email" not in data:
        errors.append("Missing required field: 'email'")
    else:
        e = data["email"]
        if not isinstance(e, str):
            errors.append(
                "email must be str, got " + type(e).__name__
            )
        elif "@" not in e:
            errors.append(
                "email must contain '@', got " + repr(e)
            )

    # Age
    if "age" not in data:
        errors.append("Missing required field: 'age'")
    else:
        a = data["age"]
        if not isinstance(a, int):
            errors.append(
                "age must be int, got " + type(a).__name__
            )
        elif a < 13 or a > 120:
            errors.append(
                "age must be between 13 and 120, got "
                + str(a)
            )

    # Password
    if "password" not in data:
        errors.append("Missing required field: 'password'")
    else:
        p = data["password"]
        if not isinstance(p, str):
            errors.append(
                "password must be str, got " + type(p).__name__
            )
        elif len(p) < 8:
            errors.append(
                "password must be at least 8 characters, got "
                + str(len(p))
            )

    if errors:
        raise ValidationError(errors)

    return data

# Valid input
good = {
    "username": "alice42",
    "email": "[email protected]",
    "age": 25,
    "password": "secure123",
}
print("Valid:", validate_user_registration(good))

# Wrong type entirely
try:
    validate_user_registration("not a dict")
except TypeError as e:
    print("Caught TypeError:", e)

# Multiple field errors
try:
    validate_user_registration({
        "username": "ab",
        "email": "not-an-email",
        "age": 10,
        "password": "123",
    })
except ValidationError as e:
    print("ValidationError:", e)
    for err in e.errors:
        print("  -", err)

# Mixed: bad format + missing field
try:
    validate_user_registration({
        "username": "user name!",
        "email": "[email protected]",
        "age": 25,
    })
except ValidationError as e:
    print("ValidationError:", e)
    for err in e.errors:
        print("  -", err)
Solution
class ValidationError(Exception):
def __init__(self, errors):
self.errors = errors
super().__init__(
str(len(errors)) + " validation error(s)"
)

def validate_user_registration(data):
if not isinstance(data, dict):
raise TypeError(
"data must be a dict, got " + repr(type(data).__name__)
)

errors = []

if "username" not in data:
errors.append("Missing required field: 'username'")
else:
u = data["username"]
if not isinstance(u, str):
errors.append(
"username must be str, got " + type(u).__name__
)
elif len(u) < 3 or len(u) > 20:
errors.append(
"username must be 3-20 characters, got "
+ str(len(u))
)
elif not u.isalnum():
errors.append(
"username must be alphanumeric, got " + repr(u)
)

if "email" not in data:
errors.append("Missing required field: 'email'")
else:
e = data["email"]
if not isinstance(e, str):
errors.append(
"email must be str, got " + type(e).__name__
)
elif "@" not in e:
errors.append(
"email must contain '@', got " + repr(e)
)

if "age" not in data:
errors.append("Missing required field: 'age'")
else:
a = data["age"]
if not isinstance(a, int):
errors.append(
"age must be int, got " + type(a).__name__
)
elif a < 13 or a > 120:
errors.append(
"age must be between 13 and 120, got " + str(a)
)

if "password" not in data:
errors.append("Missing required field: 'password'")
else:
p = data["password"]
if not isinstance(p, str):
errors.append(
"password must be str, got " + type(p).__name__
)
elif len(p) < 8:
errors.append(
"password must be at least 8 characters, got "
+ str(len(p))
)

if errors:
raise ValidationError(errors)

return data

Collecting errors before raising is the standard pattern for form validation and API input validation. The key architectural decision is the two-tier approach: raise TypeError immediately for fundamentally wrong input (not a dict), but collect field-level errors and raise once at the end. Immediate raises are for programmer errors that make further validation impossible. Collected raises are for user-facing validations where showing all problems at once saves round-trips. This is exactly how Django forms, pydantic models, and marshmallow schemas work. The custom ValidationError carries structured error data (a list) rather than just a string, making it easy for callers to display errors next to their respective form fields.

class ValidationError(Exception):
    """Carries a list of error message strings."""
    def __init__(self, errors):
        self.errors = errors
        super().__init__(
            str(len(errors)) + " validation error(s)"
        )

def validate_user_registration(data):
    """Validate a user registration dict.
    Collect ALL validation errors before raising.
    
    Required fields and rules:
    - 'username': str, 3-20 chars, alphanumeric only
    - 'email': str, must contain '@'
    - 'age': int, between 13 and 120
    - 'password': str, at least 8 chars
    
    If data is not a dict, raise TypeError immediately.
    Otherwise collect all errors into a list and raise
    a single ValidationError at the end.
    Return data unchanged if valid.
    """
    # TODO: implement
    pass
Expected Output
Valid: {'username': 'alice42', 'email': '[email protected]', 'age': 25, 'password': 'secure123'}
Caught TypeError: data must be a dict, got 'str'
ValidationError: 4 validation error(s)
  - username must be 3-20 characters, got 2
  - email must contain '@', got 'not-an-email'
  - age must be between 13 and 120, got 10
  - password must be at least 8 characters, got 3
ValidationError: 2 validation error(s)
  - username must be alphanumeric, got 'user name!'
  - Missing required field: 'password'
Hints

Hint 1: Create an empty errors list. For each field, try to validate and append error strings to the list. At the end, if errors is non-empty, raise ValidationError(errors).

Hint 2: Check for missing fields with "if field not in data" and add a missing-field error. Only validate the value if the field exists.

© 2026 EngineersOfAI. All rights reserved.