{"record":{"id":"e02df7942f82644a","repo":"rohitg00/ai-engineering-from-scratch","slug":"max-attempts-must-be-positive","errorCode":null,"errorMessage":"max_attempts must be positive","messagePattern":"max_attempts must be positive","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"certifications/claude/lessons/09-structured-output-and-defensive-parsing/code/main.py","lineNumber":103,"sourceCode":"        if \"maximum\" in schema and value > schema[\"maximum\"]:\n            issues.append(ValidationIssue(path, f\"must be at most {schema['maximum']}\"))\n    if isinstance(value, str):\n        if \"minLength\" in schema and len(value) < schema[\"minLength\"]:\n            issues.append(ValidationIssue(path, \"string is too short\"))\n        if \"maxLength\" in schema and len(value) > schema[\"maxLength\"]:\n            issues.append(ValidationIssue(path, \"string is too long\"))\n    if isinstance(value, list) and \"items\" in schema:\n        for index, item in enumerate(value):\n            issues.extend(validate(item, schema[\"items\"], f\"{path}[{index}]\"))\n    return issues\n\n\nclass BoundedExtractor:\n    \"\"\"Call a model-like function and request repair only within a fixed budget.\"\"\"\n\n    def __init__(self, generate: Callable[[str], str], schema: dict[str, Any], max_attempts: int = 2) -> None:\n        if max_attempts < 1:\n            raise ValueError(\"max_attempts must be positive\")\n        self.generate = generate\n        self.schema = schema\n        self.max_attempts = max_attempts\n\n    def extract(self, task: str) -> dict[str, Any]:\n        feedback = \"\"\n        last_error: ContractError | None = None\n        for _attempt in range(self.max_attempts):\n            prompt = task if not feedback else f\"{task}\\nRepair the previous output. Validation errors:\\n{feedback}\"\n            raw = self.generate(prompt)\n            try:\n                value = parse_and_validate(raw, self.schema)\n                if not isinstance(value, dict):\n                    raise AssertionError(\"object schema returned non-object\")\n                return value\n            except ContractError as exc:\n                last_error = exc\n                feedback = \"\\n\".join(f\"- {issue.path}: {issue.message}\" for issue in exc.issues)","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/rohitg00/ai-engineering-from-scratch/blob/39ea8a1c6d0b61f071226eff7ede4d4105fed820/certifications/claude/lessons/09-structured-output-and-defensive-parsing/code/main.py#L85-L121","documentation":"BoundedExtractor.__init__ raises ValueError('max_attempts must be positive') when max_attempts < 1. The class enforces a fixed repair budget up front so the extract loop can never run unbounded; zero or negative budgets are rejected as configuration errors at construction time, not discovered mid-loop.","triggerScenarios":"Constructing BoundedExtractor(generate, schema, max_attempts=0) or any negative value; also max_attempts=False (bool compares < 1) or a value computed from env/config that yields 0.","commonSituations":"Trying to disable retries by setting attempts to 0; deriving max_attempts from config or CLI flags that default to 0 until set; unit tests parameterizing budgets including 0.","solutions":["Pass max_attempts >= 1 (e.g. the default 2) when constructing BoundedExtractor.","If retries must be disabled, call parse_and_validate directly once instead of using a 0-attempt extractor.","Validate and clamp the config value to at least 1 before constructing.","Add a startup assertion so bad budgets fail with clearer config context."],"exampleFix":"# before\nextractor = BoundedExtractor(generate, schema, max_attempts=0)\n# ValueError: max_attempts must be positive\n\n# after\nextractor = BoundedExtractor(generate, schema, max_attempts=1)","handlingStrategy":"validation","validationCode":"def make_extractor(generate, schema, max_attempts):\n    if not isinstance(max_attempts, int) or isinstance(max_attempts, bool) or max_attempts < 1:\n        raise ValueError(\"max_attempts must be an integer >= 1\")\n    return BoundedExtractor(generate, schema, max_attempts=max_attempts)","typeGuard":null,"tryCatchPattern":"try:\n    extractor = BoundedExtractor(generate, schema, max_attempts=attempts)\nexcept ValueError as exc:\n    raise ConfigError(str(exc)) from exc  # fail at startup with config context","preventionTips":["Default max_attempts to the documented value (2) instead of deriving it from unset config.","Validate numeric config at load time with a minimum of 1.","Treat 'no retries' as a separate single-shot code path, not a zero budget."],"tags":["python","validation","constructor","configuration"],"backgroundTag":"invalid-constructor-argument","analyzedSha":"39ea8a1c6d0b61f071226eff7ede4d4105fed820","analyzedAt":"2026-08-26T03:13:46.626Z","schemaVersion":2},"datasetVersion":"2026-08-26T07:17:17.940Z"}