headroomlabs-ai/headroom · error · ValueError

expected a boolean, got {value!r}

Error message

expected a boolean, got {value!r}

What it means

ValueError from _coerce in headroom/settings_store.py when a field of type 'bool' or 'optional-bool' receives a value that is neither a real bool nor a string token in {1,true,yes,on} / {0,false,no,off,''}. Note the check is case-insensitive and strips whitespace, so ' True ' is fine, but anything else ('maybe', '2', 'enabled') raises. The ValueError is collected into SettingsValidationError.field_errors by the caller.

Source

Thrown at headroom/settings_store.py:773

    """Coerce a raw JSON/env value to the field's Python type.

    Returns ``None`` for null and empty values (empty coerces to ``None`` for
    every type except a plain ``bool``, which becomes ``False``). Raises
    ``ValueError`` on bad input so callers can surface a per-field message.
    """
    if value is None:
        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":

View on GitHub (pinned to 322425c43b)

Solutions

  1. Change the value to one of the accepted tokens: true/false, 1/0, yes/no, on/off (case-insensitive), or a real JSON boolean.
  2. If the value legitimately may be absent, make sure the field is optional-bool and pass null/'' rather than a placeholder word.
  3. If you must accept other spellings, normalize them to true/false in your own code before calling the settings API.

Example fix

# before
save({'proxy_optimize': 'ENABLED'})  # ValueError: expected a boolean, got 'ENABLED'

# after
save({'proxy_optimize': 'on'})  # or True / 'true'
Defensive patterns

Strategy: validation

Validate before calling

TRUE = {'1', 'true', 'yes', 'on'}
FALSE = {'0', 'false', 'no', 'off', ''}

def valid_bool_token(v) -> bool:
    return isinstance(v, bool) or str(v).strip().lower() in TRUE | FALSE

Type guard

def is_bool_like(v) -> bool:
    return isinstance(v, bool) or str(v).strip().lower() in {'1','true','yes','on','0','false','no','off',''}

Try / catch

from headroom.settings_store import SettingsValidationError
try:
    store.save(payload)
except SettingsValidationError as e:
    for key, msg in e.field_errors.items():
        if 'expected a boolean' in msg:
            payload[key] = bool(payload[key])  # or fix upstream
    store.save(payload)

Prevention

When it happens

Trigger: Saving a bool setting with an unsupported string (HEADROOM-style env value 'ENABLED', 'y', '2') or a non-string non-bool JSON value like 2 or [True]. Example: save({'optimize': 'sometimes'}) where 'optimize' is a bool field.

Common situations: Operators copying boolean conventions from other tools ('y'/'enabled'/'on-request'); JSON payloads from UIs that send 1/2 instead of true/false; shell scripts passing $FLAG that is empty-but-quoted oddly combined with a plain bool type (empty string maps to False for 'bool' but None for 'optional-bool').

Related errors


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