Exercise 3 solution import re LINE = re.compile(r""" ^\s* (?: \[(?P
[^\]]+)\] # [section] | (?P\w+)\s*=\s*(?P[^#\n]*?) # key = value )? \s*(?:\#.*)?$ # optional comment """, re.VERBOSE) def parse_config(text): result, current = {}, None for number, line in enumerate(text.splitlines(), 1): m = LINE.match(line) if m is None: raise ValueError(f"line {number}: cannot read {line!r}") if m["section"]: current = result.setdefault(m["section"].strip(), {}) elif m["key"]: if current is None: raise ValueError(f"line {number}: key outside a section") current[m["key"]] = m["value"] return result config = """ # demo [server] host = example.org # main host port = 8080 [paths] root = /var/data """ print(parse_config(config)) for bad in ["[a]\nthis is not valid", "x = 1"]: try: parse_config(bad) except ValueError as e: print(e) # {'server': {'host': 'example.org', 'port': '8080'}, 'paths': {'root': '/var/data'}} # line 2: cannot read 'this is not valid' # line 1: key outside a section (All lines were run and printed exactly what is shown.) Explanation: a VERBOSE pattern with three named parts reads like a small grammar. Each line either is a section header, a key = value pair, or is blank or a comment. Anything else fails to match, which is how bad lines are caught.