headroomlabs-ai/headroom · error · ValueError

expected an integer, got {value!r}

Error message

expected an integer, got {value!r}

What it means

ValueError from _coerce in headroom/settings_store.py when an 'int' field receives a float that has a fractional part (value.is_integer() is False), e.g. 1.5. Whole floats like 2.0 are accepted and truncated via int(). The error is surfaced per-field through SettingsValidationError.field_errors.

Source

Thrown at headroom/settings_store.py:780

        return None
    if field.type in ("bool", "optional-bool"):
        if isinstance(value, bool):
            return value
        token = str(value).strip().lower()
        if field.type == "optional-bool" and token == "":
            return None
        if token in ("1", "true", "yes", "on"):
            return True
        if token in ("0", "false", "no", "off", ""):
            return False
        raise ValueError(f"expected a boolean, got {value!r}")
    if field.type in ("int", "float"):
        if isinstance(value, bool):  # bool is an int subclass — reject explicitly
            raise ValueError(f"expected a number, got {value!r}")
        number: int | float
        if field.type == "int":
            if isinstance(value, float) and not value.is_integer():
                raise ValueError(f"expected an integer, got {value!r}")
            number = int(value)
        else:
            number = float(value)
            if not math.isfinite(number):
                raise ValueError(f"expected a finite number, got {value!r}")
        if field.minimum is not None and number < field.minimum:
            raise ValueError(f"must be >= {field.minimum}")
        if field.maximum is not None and number > field.maximum:
            raise ValueError(f"must be <= {field.maximum}")
        return number
    if field.type == "enum":
        token = str(value)
        if token not in field.choices:
            raise ValueError(f"{token!r} not one of {list(field.choices)}")
        return token
    if field.type == "csv-list":
        tokens = value if isinstance(value, list | tuple) else str(value).split(",")
        tokens = [str(token).strip() for token in tokens]

View on GitHub (pinned to 322425c43b)

Solutions

  1. Round or floor the value to a whole number before saving (int(value) if it should truncate).
  2. Fix the upstream computation so it yields an integer (use // or round()).
  3. If a fractional value is legitimate, the field must be a 'float' type in the registry — check the field's declared type.

Example fix

# before
save({'max_tokens': 4096 * 0.75})  # 3072.0 fine; 4095.5 raises

# after
save({'max_tokens': int(4096 * 0.75)})
Defensive patterns

Strategy: validation

Validate before calling

def valid_int(v) -> bool:
    return not isinstance(v, bool) and (
        isinstance(v, int) or (isinstance(v, float) and v.is_integer())
    )

Type guard

def is_int_like(v) -> bool:
    return not isinstance(v, bool) and (isinstance(v, int) or (isinstance(v, float) and float(v).is_integer()))

Try / catch

except SettingsValidationError as e:
    for key, msg in e.field_errors.items():
        if 'expected an integer' in msg:
            payload[key] = round(payload[key])
    store.save(payload)

Prevention

When it happens

Trigger: save({'port': 8080.5}), or a computed value like count * 0.5 assigned to an integer field; JSON payloads from JS where every number is a float and one carries a fraction.

Common situations: JavaScript clients (all numbers are floats) sending fractional values; spreadsheet- or formula-derived configs; dividing values then forgetting to round.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/b9609a63dbe3dbc2. Report an issue: GitHub.