Skip to content

← From Everyday Python to Production Code step 5 of 223

Medium Primitives

Placement 3/6: Turn Parse Errors into Your Own Exception

Solve this without the hints and you can skim the error-handling track.

You are reading a config file of key=number lines. When a line is bad, the caller should get one exception type that means "bad config line", whatever went wrong underneath. The starter gives you that type, ConfigError. Code that catches it knows the config is at fault, not some unrelated bug.

When the number does not parse, int() raises ValueError. Keep that error attached to your ConfigError with exception chaining:

except ValueError as exc:
    raise ConfigError(line) from exc

from exc stores the original exception in the new one's __cause__ attribute, and the traceback shows both.

Parse one line

Write parse_line(line: str) -> tuple[str, int]. Split line at the first =, and strip whitespace from each part. Then check, in this order:

  1. If there is no =, or the key is empty after stripping, raise ConfigError(line) without from, so __cause__ is None.
  2. Convert the value with int(value). If that raises ValueError, raise ConfigError(line) from exc.
  3. Otherwise return the stripped key and the integer.

Always pass the original, unstripped line to ConfigError. Any non-empty key is fine. Don't add support for comments or any other syntax.

int() accepts a sign, leading zeros and underscores between digits, so "n=+0" gives ("n", 0) and "n=1_000" gives ("n", 1000). An empty value, "1.5" and "1=2" all fail to convert. "=bad" fails the key check first, so it has no cause.

ConfigError is a subclass of ValueError. Put the try around the int() call alone. A wider try would catch the ConfigError you raise in the key check and chain it to a second one.

Collect the results

Write solve(lines: list[str]) -> dict[str, list[str]]. Call parse_line on every line, catch ConfigError for that line, and carry on with the next. Catch only ConfigError, so a real bug still shows up. Return two lists that keep the input order and any duplicates:

  • "ok": one f"{key}={value}" per good line, using the parsed values.
  • "errors": one f"{line}|ConfigError|{cause}" per bad line. line is the raw line, spaces and all, with nothing escaped. cause is the class name of exc.__cause__, or "-" when it is None.
solve([" n=+0 ", "bad=x", "missing", "ok=3"])
# {"ok": ["n=0", "ok=3"],
#  "errors": ["bad=x|ConfigError|ValueError", "missing|ConfigError|-"]}

An empty list returns {"ok": [], "errors": []}.