headroomlabs-ai/headroom · error · TypeError

{cls!r} must be a dataclass

Error message

{cls!r} must be a dataclass

What it means

TypeError raised by _field_contract() in headroom/testing/harness.py when it is called with a class that is not a Python dataclass (is_dataclass(cls) is False, line 407). The function walks cls.__dataclass_fields__ to build the (owner, name, type, default) FieldContract tuple used by the harness's config-schema checks, so only dataclasses are accepted — plain classes, NamedTuples, pydantic models, or TypedDicts all fail this check.

Source

Thrown at headroom/testing/harness.py:405

            "config_env_var": self.config_env_var,
        }

    def validate(self) -> None:
        """Fail if the environment does not round-trip the full config payload."""

        raw = self.env.get(self.config_env_var)
        if raw is None:
            raise ValueError(f"deployment env missing {self.config_env_var}")
        parsed = json.loads(raw)
        if parsed != self.config_payload:
            raise ValueError(f"{self.config_env_var} does not match config_payload")


def _field_contract(
    owner: Literal["headroom", "proxy"], cls: type[Any]
) -> tuple[FieldContract, ...]:
    if not is_dataclass(cls):
        raise TypeError(f"{cls!r} must be a dataclass")
    out: list[FieldContract] = []
    for field in cls.__dataclass_fields__.values():
        default: Any = MISSING
        if field.default is not MISSING:
            default = field.default
        elif field.default_factory is not MISSING:  # type: ignore[attr-defined]
            default = "<factory>"
        out.append(
            FieldContract(
                owner=owner,
                name=field.name,
                type_repr=str(field.type),
                has_default=default is not MISSING,
                default_repr=None if default is MISSING else repr(default),
            )
        )
    return tuple(out)

View on GitHub (pinned to 322425c43b)

Solutions

  1. Decorate the class with @dataclass (or @dataclass(kw_only=True) as the neighboring configs use).
  2. If the class is third-party (pydantic), wrap its fields in a small stdlib dataclass mirror for the contract check.
  3. Confirm with dataclasses.is_dataclass(cls) in a unit test for every class passed to _field_contract.

Example fix

# before
class ProxyCfg:  # plain class
    mode: str = 'token'
_field_contract('proxy', ProxyCfg)  # TypeError

# after
from dataclasses import dataclass

@dataclass
class ProxyCfg:
    mode: str = 'token'
_field_contract('proxy', ProxyCfg)
Defensive patterns

Strategy: type-guard

Validate before calling

from dataclasses import is_dataclass

assert is_dataclass(cls), f'{cls!r} must be a dataclass'
contract = _field_contract('proxy', cls)

Type guard

from dataclasses import is_dataclass
from typing import Type, Any

def is_dataclass_type(cls: Type[Any]) -> bool:
    return is_dataclass(cls)

Prevention

When it happens

Trigger: Passing a plain class or a pydantic BaseModel to _field_contract(owner='headroom', cls=SomeConfig) instead of a @dataclass-decorated config class; refactoring a config dataclass into a pydantic model without updating the harness contract call.

Common situations: Migrating config classes to pydantic/attrs for validation while the testing harness still expects stdlib dataclasses; contributing new owner configs to the harness and forgetting the decorator.

Related errors


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