Skip to content
VLSI Mentor

Python Part II · Module 21 · Structured Engineering Data and Configuration

Validating Configuration Before Anything Runs

A regression config is read in the first hundred milliseconds of a job that runs for four hours. Every mistake in it is therefore knowable before anything starts, and the default behaviour of a script is to discover it as late as possible — at the point where the bad value is finally used, in a worker process, at three in the morning, with a traceback that names a line in the runner rather than a line in the file somebody edited. This chapter closes that gap. The checks are short, they need nothing installed, and the hardest part of writing them is knowing when to stop.

Advanced18 min readPythonPart IIData FormatsVerification FlowAutomation

Part II · Module 21 · Chapter 21.7 · Validating Configuration Before Anything Runs

1. When the Mistake Is Found

A config with "timeout_s": -1 in it. Here is what each of three scripts does with it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CONFIG = {"sim": "xcelium", "timeout_s": -1, "tests": ["axi_smoke", "axi_burst"]}


def no_checking(cfg):
    """Discovers it when the value is used — per test, deep in the run."""
    started = []
    for test in cfg["tests"]:
        started.append(test)
        if cfg["timeout_s"] <= 0:
            raise ValueError(f"timeout expired immediately for {test}")
    return started


def checked_at_load(cfg):
    """Discovers it before anything starts."""
    if cfg["timeout_s"] <= 0:
        raise ValueError(f"timeout_s must be positive, got {cfg['timeout_s']}")
    return list(cfg["tests"])


try:
    no_checking(CONFIG)
except ValueError as exc:
    print("no checking    :", exc)

try:
    checked_at_load(CONFIG)
except ValueError as exc:
    print("checked at load:", exc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no checking    : timeout expired immediately for axi_smoke
checked at load: timeout_s must be positive, got -1

Two messages about the same defect. The first names a test and describes a symptom; it arrives after the flow has set up a run directory, elaborated a design and launched a simulator. The second names the key, shows the value, and arrives before any of that.

In a real sweep the first version is worse than this example suggests, because the failure happens once per job — so a 400-test regression produces 400 identical failures, a results table full of them, and a morning spent working out that they share one cause.

The canonical purpose puts it as a time: load time, not four hours into an overnight regression. The config is read in the first hundred milliseconds. Everything wrong with it is knowable then.

2. Four Kinds of Check

The canonical purpose names three defects, and they are three different kinds of question:

DefectKindThe question
the missing keystructureis it there, and is it the right type
the misspelled simulatorenumerationis this value one of the allowed ones
the negative timeoutrangeis this value sensible
—relationshipdo two keys make sense together

The fourth is not in the canonical sentence and belongs in any real validator, because it is the one no per-key check can express: waves: true with wave_file: null is two individually valid keys and one unrunnable config.

3. An Error Type of Your Own

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ConfigError(Exception):
    """A configuration file is wrong. Not a bug in the flow."""


SUPPORTED = ["xcelium", "vcs", "questa", "verilator"]


def check_sim(cfg):
    if cfg["sim"] not in SUPPORTED:
        raise ConfigError(f"unknown sim {cfg['sim']!r}")


try:
    check_sim({"sim": "xcelum"})
except ConfigError as exc:
    print("ConfigError:", exc)

print("is an Exception:", issubclass(ConfigError, Exception))
print("not a ValueError:", not issubclass(ConfigError, ValueError))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ConfigError: unknown sim 'xcelum'
is an Exception: True
not a ValueError: True

That is the canonical annotation, and the custom exception type is Chapter 14.8's. It earns its three lines for a reason specific to a flow: ConfigError means the person who edited the file made a mistake, and every other exception means the flow did. A wrapper script can catch ConfigError, print the message without a traceback, and exit — while letting a genuine bug produce the full traceback a developer needs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ConfigError(Exception):
    """A configuration file is wrong. Not a bug in the flow."""


def run(cfg):
    if cfg.get("sim") != "xcelium":
        raise ConfigError(f"unknown sim {cfg.get('sim')!r}")
    return cfg["tests"]          # a real bug if 'tests' is absent


def main(cfg):
    try:
        return "ran", run(cfg)
    except ConfigError as exc:
        return "config", str(exc)


print(main({"sim": "xcelum"}))
try:
    main({"sim": "xcelium"})
except KeyError as exc:
    print("('bug', KeyError", exc, ")")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
('config', "unknown sim 'xcelum'")
('bug', KeyError 'tests' )

The config mistake is handled and reported as text; the missing tests key — a bug in run, not in the file — escapes with its traceback intact. That separation is the whole reason not to raise ValueError for both.

Note !r in the message. unknown sim 'xcelum' with quotes shows a trailing space or a stray tab that unknown sim xcelum would hide — Chapter 19.17 Step 1's habit, in an error message.

4. Report Every Problem at Once

The canonical annotation raises on the first failure. For one check that is right; for a validator it is not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ConfigError(Exception):
    pass


SUPPORTED = ["xcelium", "vcs", "questa"]
BAD = {"sim": "xcelum", "timeout_s": -1, "waves": "true"}


def raise_first(cfg):
    if "seeds" not in cfg:
        raise ConfigError("missing required key 'seeds'")
    if cfg["sim"] not in SUPPORTED:
        raise ConfigError(f"unknown sim {cfg['sim']!r}")
    if cfg["timeout_s"] <= 0:
        raise ConfigError(f"timeout_s must be positive, got {cfg['timeout_s']}")


def collect_all(cfg):
    problems = []
    if "seeds" not in cfg:
        problems.append("missing required key 'seeds'")
    if cfg.get("sim") not in SUPPORTED:
        problems.append(f"unknown sim {cfg.get('sim')!r}")
    if isinstance(cfg.get("timeout_s"), int) and cfg["timeout_s"] <= 0:
        problems.append(f"timeout_s must be positive, got {cfg['timeout_s']}")
    if not isinstance(cfg.get("waves"), bool):
        problems.append(f"waves must be true or false, got {cfg.get('waves')!r}")
    return problems


try:
    raise_first(BAD)
except ConfigError as exc:
    print("raise on first:", exc)

print()
print("collect all   :")
for problem in collect_all(BAD):
    print("   -", problem)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
raise on first: missing required key 'seeds'

collect all   :
   - missing required key 'seeds'
   - unknown sim 'xcelum'
   - timeout_s must be positive, got -1
   - waves must be true or false, got 'true'

One problem or four. The config has four, and the engineer using the first validator fixes one, re-runs, finds the next, fixes it, re-runs — four round trips through a job queue to learn what one run could have said.

This is Chapter 19.18 Section 2's rule and Chapter 21.2 Section 9's, and it is the same rule for the same reason: the pattern of failures is the diagnosis. Four problems in one config suggests the file was copied from another project; one problem suggests a typo.

The validator still raises — once, at the end, with everything in the message:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ConfigError(Exception):
    pass


def validate(cfg):
    problems = []
    if "seeds" not in cfg:
        problems.append("missing required key 'seeds'")
    if cfg.get("timeout_s", 1) <= 0:
        problems.append(f"timeout_s must be positive, got {cfg.get('timeout_s')}")
    if problems:
        joined = "\n  - ".join(problems)
        raise ConfigError(f"regress.json has {len(problems)} problem(s):\n  - {joined}")
    return cfg


try:
    validate({"timeout_s": -1})
except ConfigError as exc:
    print(exc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
regress.json has 2 problem(s):
  - missing required key 'seeds'
  - timeout_s must be positive, got -1

The file is named, the count is given, and each problem is on its own line. A colleague can fix all of it in one pass.

5. Types, and the Boolean That Is an Integer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
REQUIRED = {"sim": str, "timeout_s": int, "seeds": int, "waves": bool}

CONFIGS = {
    "good      ": {"sim": "xcelium", "timeout_s": 7200, "seeds": 4, "waves": False},
    "timeout=on": {"sim": "xcelium", "timeout_s": True, "seeds": 4, "waves": False},
    "seeds='4' ": {"sim": "xcelium", "timeout_s": 7200, "seeds": "4", "waves": False},
}


def naive_types(cfg):
    return [f"{k}: expected {t.__name__}, got {type(cfg[k]).__name__}"
            for k, t in REQUIRED.items() if not isinstance(cfg[k], t)]


def careful_types(cfg):
    problems = []
    for key, want in REQUIRED.items():
        value = cfg[key]
        if want is int and isinstance(value, bool):
            problems.append(f"{key}: expected int, got bool {value!r}")
        elif not isinstance(value, want):
            problems.append(f"{key}: expected {want.__name__}, got {type(value).__name__} {value!r}")
    return problems


print("isinstance(True, int):", isinstance(True, int))
print()
for label, cfg in CONFIGS.items():
    print(f"{label}  naive={naive_types(cfg)}")
    print(f"{' ' * len(label)}  careful={careful_types(cfg)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
isinstance(True, int): True

good        naive=[]
            careful=[]
timeout=on  naive=[]
            careful=['timeout_s: expected int, got bool True']
seeds='4'   naive=['seeds: expected int, got str']
            careful=["seeds: expected int, got str '4'"]

isinstance(True, int) is True, because bool is a subclass of int in Python. So a config containing "timeout_s": true passes a naive integer check, and the value True then behaves as 1 — a one-second timeout, in a flow that expected thousands.

That is not a contrived config. JSON and TOML both have a real boolean, and timeout_s: true is what you get from a search-and-replace, or from an engineer who thought the key enabled a timeout rather than setting it.

The exclusion is one clause — if want is int and isinstance(value, bool) — and it is only needed in that direction. A bool field containing 1 is caught by the ordinary check, because int is not a subclass of bool.

The seeds: "4" row is the other common one, and it is what Chapter 21.2 Section 5 produced from every CSV field. A config that has been through a CSV, an environment variable or a command line arrives as strings, and a type check is the only thing between that and "4" * 2 being "44".

6. Enumerations, and Saying What You Meant

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import difflib

SUPPORTED = ["xcelium", "vcs", "questa", "verilator"]

for attempt in ["xcelium", "xcelum", "qesta", "VCS", "icarus"]:
    if attempt in SUPPORTED:
        print(f"{attempt:<10} ok")
        continue
    close = difflib.get_close_matches(attempt, SUPPORTED, n=1, cutoff=0.6)
    hint = f"did you mean {close[0]!r}?" if close else f"choose from {SUPPORTED}"
    print(f"{attempt:<10} unknown — {hint}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
xcelium    ok
xcelum     unknown — did you mean 'xcelium'?
qesta      unknown — did you mean 'questa'?
VCS        unknown — choose from ['xcelium', 'vcs', 'questa', 'verilator']
icarus     unknown — choose from ['xcelium', 'vcs', 'questa', 'verilator']

difflib.get_close_matches is in the standard library — Chapter 13.4 — and turns an error into a fix. Two of the five got a specific suggestion and the message cost one line.

Read the VCS row, because it shows the limit. The comparison is case-sensitive, so an attempt that differs only in case scores too low to suggest. For a simulator name that is exactly the mistake somebody makes, and the fix is to normalise before comparing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import difflib

SUPPORTED = ["xcelium", "vcs", "questa", "verilator"]
BY_LOWER = {name.lower(): name for name in SUPPORTED}


def resolve_sim(attempt):
    """The canonical name, a suggestion, or the full list."""
    if attempt in SUPPORTED:
        return attempt, None
    if attempt.lower() in BY_LOWER:
        return None, f"did you mean {BY_LOWER[attempt.lower()]!r}? (names are lower case)"
    close = difflib.get_close_matches(attempt.lower(), SUPPORTED, n=1, cutoff=0.6)
    if close:
        return None, f"did you mean {close[0]!r}?"
    return None, f"choose from {SUPPORTED}"


for attempt in ["vcs", "VCS", "Xcelium", "xcelum", "icarus"]:
    name, hint = resolve_sim(attempt)
    print(f"{attempt:<10} {'ok' if name else 'unknown — ' + hint}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vcs        ok
VCS        unknown — did you mean 'vcs'? (names are lower case)
Xcelium    unknown — did you mean 'xcelium'? (names are lower case)
xcelum     unknown — did you mean 'xcelium'?
icarus     unknown — choose from ['xcelium', 'vcs', 'questa', 'verilator']

Note that resolve_sim rejects VCS rather than accepting it. That is a choice, and the reasoning is Chapter 21.6's: if the validator accepts both spellings, every consumer downstream has to handle both, or the one that does a cfg["sim"] == "vcs" comparison quietly takes the wrong branch. One spelling in the file, with a message that says which.

7. Ranges

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RANGES = {
    "timeout_s": (1, 86400),
    "seeds": (1, 10000),
    "jobs": (1, 512),
}


def check_ranges(cfg):
    problems = []
    for key, (low, high) in RANGES.items():
        if key not in cfg:
            continue
        value = cfg[key]
        if not isinstance(value, int) or isinstance(value, bool):
            continue
        if not low <= value <= high:
            problems.append(f"{key} must be between {low} and {high}, got {value}")
    return problems


for cfg in [{"timeout_s": 7200, "seeds": 4, "jobs": 64},
            {"timeout_s": -1, "seeds": 0, "jobs": 100000},
            {"timeout_s": 86401}]:
    print(cfg, "->", check_ranges(cfg))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'timeout_s': 7200, 'seeds': 4, 'jobs': 64} -> []
{'timeout_s': -1, 'seeds': 0, 'jobs': 100000} -> ['timeout_s must be between 1 and 86400, got -1', 'seeds must be between 1 and 10000, got 0', 'jobs must be between 1 and 512, got 100000']
{'timeout_s': 86401} -> ['timeout_s must be between 1 and 86400, got 86401']

Three things about those bounds.

The lower bound is the one that catches real mistakes. timeout_s: -1 and seeds: 0 are both the result of arithmetic somewhere upstream, or of a sentinel value somebody meant as "unlimited".

The upper bound is a judgment and should be generous. 86400 is a day, and a config asking for two days is more likely to be a typo than a requirement — but if somebody has a legitimate three-day formal run, a validator that refuses it is an obstacle rather than a check. Set the bound where a value is certainly wrong, not where it is unusual.

The checks skip what is absent or the wrong type, because Section 5 already reported that and a value failing two checks produces two messages for one mistake.

8. Relationships Between Keys

The kind of defect no per-key check can see:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CASES = [
    {"waves": False, "wave_file": None, "sim": "xcelium", "jobs": 8, "seeds": 4},
    {"waves": True, "wave_file": None, "sim": "xcelium", "jobs": 8, "seeds": 4},
    {"waves": True, "wave_file": "dump.fsdb", "sim": "verilator", "jobs": 8, "seeds": 4},
    {"waves": False, "wave_file": None, "sim": "xcelium", "jobs": 64, "seeds": 1},
]

FSDB_CAPABLE = {"xcelium", "vcs", "questa"}


def check_relationships(cfg):
    problems = []
    if cfg.get("waves") and cfg.get("wave_file") is None:
        problems.append("waves is true but wave_file is not set")
    if (cfg.get("wave_file") or "").endswith(".fsdb") and cfg.get("sim") not in FSDB_CAPABLE:
        problems.append(f"wave_file is .fsdb but sim {cfg.get('sim')!r} cannot write fsdb")
    if cfg.get("jobs", 1) > cfg.get("seeds", 1) * 8:
        problems.append(f"jobs={cfg.get('jobs')} is far more than seeds={cfg.get('seeds')} needs")
    return problems


for index, cfg in enumerate(CASES, 1):
    print(index, check_relationships(cfg))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 []
2 ['waves is true but wave_file is not set']
3 ["wave_file is .fsdb but sim 'verilator' cannot write fsdb"]
4 ['jobs=64 is far more than seeds=1 needs']

Every key in cases 2, 3 and 4 is individually valid. The configs are not.

Case 3 is the one worth dwelling on: .fsdb is a proprietary waveform format, verilator does not write it, and both wave_file and sim are perfectly legal values. Only a check that looks at two keys together can see it — and the alternative is finding out when the simulator exits with an unrecognised option, per job, four hours in.

(cfg.get("wave_file") or "") is doing work, and the obvious spelling does not. cfg.get("wave_file", "") returns the default only when the key is absent — and in case 2 the key is present with the value None, so it returns None and None.endswith raises. The or "" covers both, because None is falsy. That is Chapter 19.10 Section 6's rule exactly: or for a display default, and never confuse "absent" with "present and empty".

Case 4 is a different sort of check — not wrong but suspicious, and a flow might reasonably warn rather than refuse. Section 9 is about that line.

9. Knowing When to Stop

A validator that rejects everything it does not recognise is a validator that makes the config unextendable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
KNOWN = {"sim", "timeout_s", "seeds", "waves", "wave_file"}

CFG = {"sim": "xcelium", "timeout_s": 7200, "seeds": 4,
       "coverage_db": "cov/axi.ucdb", "_comment": "raised for GLS"}


def strict(cfg):
    return [f"unknown key {k!r}" for k in sorted(cfg) if k not in KNOWN]


def warn_only(cfg):
    unknown = [k for k in sorted(cfg) if k not in KNOWN and not k.startswith("_")]
    return [f"note: key {k!r} is not used by this script" for k in unknown]


print("strict   :", strict(CFG))
print("warn only:", warn_only(CFG))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
strict   : ["unknown key '_comment'", "unknown key 'coverage_db'"]
warn only: ["note: key 'coverage_db' is not used by this script"]

The strict version rejects two keys and both rejections are wrong. _comment is Chapter 21.4 Section 7's comment convention. coverage_db is a real setting that another script in the same flow reads — and the validator in this script has no business refusing it.

That is the general shape of over-validation in a flow: several tools share one config, each uses a subset, and a validator that demands to recognise every key makes adding a setting a change to every tool.

Three rules that keep a validator useful:

Validate what you use. If this script never reads coverage_db, it has no opinion about it.

Warn, do not reject, on the unknown. A note catches the genuine typo — timout_s instead of timeout_s — without blocking a key that belongs to somebody else. And a typo'd key is otherwise invisible, because the misspelling means the real key is simply absent and takes its default.

Reject what is certainly wrong, warn about what is merely odd. timeout_s: -1 cannot work. jobs: 64 for one seed is wasteful and might be deliberate.

10. The Validator

Azvya Education Pvt. Ltd.VLSI Mentor
regress.json
{
  "sim": "xcelum",
  "timeout_s": true,
  "seeds": 4,
  "waves": true,
  "wave_file": null,
  "coverage_db": "cov/axi.ucdb"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import difflib
import json
from pathlib import Path


class ConfigError(Exception):
    """A configuration file is wrong. Not a bug in the flow."""


SUPPORTED = ["xcelium", "vcs", "questa", "verilator"]
BY_LOWER = {name.lower(): name for name in SUPPORTED}
REQUIRED = {"sim": str, "timeout_s": int, "seeds": int, "waves": bool}
RANGES = {"timeout_s": (1, 86400), "seeds": (1, 10000)}
USED = set(REQUIRED) | {"wave_file"}


def validate(cfg, source):
    """Every problem with this config, as a list of sentences."""
    problems = []

    for key, want in REQUIRED.items():
        if key not in cfg:
            problems.append(f"missing required key {key!r}")
        elif want is int and isinstance(cfg[key], bool):
            problems.append(f"{key}: expected int, got bool {cfg[key]!r}")
        elif not isinstance(cfg[key], want):
            problems.append(
                f"{key}: expected {want.__name__}, got {type(cfg[key]).__name__} {cfg[key]!r}")

    sim = cfg.get("sim")
    if isinstance(sim, str) and sim not in SUPPORTED:
        if sim.lower() in BY_LOWER:
            problems.append(f"unknown sim {sim!r} — did you mean {BY_LOWER[sim.lower()]!r}?")
        else:
            close = difflib.get_close_matches(sim.lower(), SUPPORTED, n=1, cutoff=0.6)
            hint = f"did you mean {close[0]!r}?" if close else f"choose from {SUPPORTED}"
            problems.append(f"unknown sim {sim!r} — {hint}")

    for key, (low, high) in RANGES.items():
        value = cfg.get(key)
        if isinstance(value, int) and not isinstance(value, bool) and not low <= value <= high:
            problems.append(f"{key} must be between {low} and {high}, got {value}")

    if cfg.get("waves") and cfg.get("wave_file") is None:
        problems.append("waves is true but wave_file is not set")

    notes = [f"note: key {k!r} is not used by this script"
             for k in sorted(cfg) if k not in USED and not k.startswith("_")]

    if problems:
        joined = "\n  - ".join(problems + notes)
        raise ConfigError(f"{source}: {len(problems)} problem(s)\n  - {joined}")
    return notes


cfg = json.loads(Path("regress.json").read_text(encoding="utf-8"))
try:
    validate(cfg, "regress.json")
except ConfigError as exc:
    print(exc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
regress.json: 3 problem(s)
  - timeout_s: expected int, got bool True
  - unknown sim 'xcelum' — did you mean 'xcelium'?
  - waves is true but wave_file is not set
  - note: key 'coverage_db' is not used by this script

Three problems and a note, from one read of one file, before a single simulator is launched. Three of Section 2's four kinds are represented — a type, an enumeration and a relationship — and each message names the key, shows the value and, where it can, says what to write instead. The range check found nothing because timeout_s had already been reported as the wrong type, which is Section 7's skip doing its job.

Two deliberate details in the assembly. The note is appended to the message but not counted in len(problems), so an otherwise-valid config with an unrecognised key is reported and not rejected. And validate returns the notes on success, so a caller can print them when the config is fine.

Where this goes in a flow is the last decision, and it is Chapter 20.1's: validate is processing — it produces a list of facts — and whether a note becomes a warning or a refusal is policy belonging to the caller.

11. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import difflib

SUPPORTED = ["xcelium", "vcs"]

# A
print("A:", isinstance(True, int), isinstance(1, bool))

# B
print("B:", isinstance(True, int) and not isinstance(True, bool))

# C
print("C:", difflib.get_close_matches("xcelum", SUPPORTED, n=1, cutoff=0.6))

# D
print("D:", difflib.get_close_matches("XCELIUM", SUPPORTED, n=1, cutoff=0.6))

# E
print("E:", difflib.get_close_matches("xcelium".lower(), SUPPORTED, n=1))

# F
print("F:", {}.get("wave_file", "").endswith(".fsdb"))

# G
cfg = {"waves": True, "wave_file": None}
print("G:", bool(cfg.get("waves")), cfg.get("wave_file") is None)

# H
print("H:", 1 <= 0 <= 86400, 1 <= 7200 <= 86400)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: True False
B: False
C: ['xcelium']
D: []
E: ['xcelium']
F: False
G: True True
H: False True

A — a bool is an int and an int is not a bool. The asymmetry is why the exclusion is needed in only one direction.

B — so no value is an int-but-not-a-bool by that test; the check has to be written the other way round, as Section 5 does.

C — a one-character typo is matched.

D — upper case is not, because the comparison is case-sensitive. The VCS row from Section 6.

E — lowering first finds it, which is the fix.

F — .get(key, "") makes endswith safe on an absent key, where .get(key) would return None and raise.

G — the relationship check's two halves, each individually true.

H — the range test rejects 0 and accepts 7200.

12. Exercises

Exercise 1 — two messages. Write the same defect as a late failure and a load-time failure. Say what each message tells somebody at 3am.

Exercise 2 — per job. For a 400-test regression, say how many times the late version fails and what the results table looks like.

Exercise 3 — four kinds. For your own config, write one example of each of the four kinds of check from Section 2.

Exercise 4 — your own error type. Define ConfigError and show a wrapper that prints its message without a traceback while letting a KeyError through.

Exercise 5 — quotes in the message. Write an error message with and without !r for a value with a trailing space. Say which one you could debug.

Exercise 6 — one or four. Write a raise-on-first and a collect-all validator for a config with three defects. Count the round trips each costs.

Exercise 7 — the boolean. Put timeout_s: true in a config and run a naive isinstance(v, int) check. Record the result and write the exclusion.

Exercise 8 — the quoted number. Put seeds: "4" in a config. Decide whether your validator rejects or coerces, and justify it from where the value came from.

Exercise 9 — did you mean. Use difflib.get_close_matches on four misspellings of a value in your flow. Report which got a suggestion.

Exercise 10 — the case gap. Find a misspelling that gets no suggestion and fix it by normalising. Say why you would still reject the input.

Exercise 11 — generous bounds. Choose upper bounds for two numeric keys. For each, name a legitimate value that is unusual and confirm your bound allows it.

Exercise 12 — two keys together. Write a relationship check for your flow where both keys are individually valid. Say what the symptom would be without it.

Exercise 13 — strict versus warn. Run a strict unknown-key check against a config shared with another tool. List every key it wrongly rejects.

Exercise 14 — the misc dict. Describe how your flow would end up with an unvalidated free-form section, starting from a strict validator.

13. Summary

  • A config is read in the first hundred milliseconds of a four-hour job, so everything wrong with it is knowable before anything starts.
  • A late failure names a symptom and a test; a load-time failure names the key and the value — and the late one happens once per job.
  • Four kinds of check: structure, enumeration, range, and relationship. Only the first three are in the canonical sentence.
  • A ConfigError of your own separates the file being wrong from the flow being wrong, so a wrapper can print one without a traceback and let the other produce one.
  • Use !r in messages so a trailing space is visible.
  • Collect every problem and raise once, with the file named and one problem per line — four round trips become one.
  • isinstance(True, int) is True, so timeout_s: true passes a naive integer check and behaves as a one-second timeout.
  • The exclusion is needed in one direction only, because int is not a subclass of bool.
  • Reject a quoted number in a file; coerce one at a command-line boundary. Never both for the same value.
  • difflib.get_close_matches turns an enumeration error into a fix, and it is in the standard library.
  • It is case-sensitive, so normalise before matching — and still reject, so downstream code sees one spelling.
  • Set range bounds where a value is certainly wrong, not where it is unusual. The lower bound catches the real mistakes.
  • Skip checks for values already reported as missing or wrongly typed, or one mistake produces several messages.
  • A relationship check sees what no per-key check can — waves: true with no wave_file, or an .fsdb file with a simulator that cannot write one.
  • Validate what you use; warn, do not reject, on unknown keys. Several tools share one config, and a strict validator makes every new setting a change to all of them.
  • Over-validation produces a misc dict that nothing checks at all.
  • Validation is processing, not policy — it returns facts, and the caller decides what is fatal.

That completes Module 21. A flow now has a result record whose fields are agreed, tables other tools can read and write, a machine format for anything nested, a config engineers can edit with comments, layered settings whose precedence is written down and tested, and a validator that rejects a bad config in the first second rather than the fourth hour. Module 22 gives the scripts that use all of it a command-line interface worth the name.

Where this fits

Part of the Python curriculum.