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
- Decorate the class with @dataclass (or @dataclass(kw_only=True) as the neighboring configs use).
- If the class is third-party (pydantic), wrap its fields in a small stdlib dataclass mirror for the contract check.
- 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
- Keep harness config classes as stdlib dataclasses.
- Add a test asserting is_dataclass for every class registered with the harness.
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
- expected a number, got {value!r}
- deployment env missing {self.config_env_var}
- {self.config_env_var} does not match config_payload
- mode must be 'token' or 'cache'
- {name} is read-only; mutate its fields instead
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/7b8fafeaf576b80e.
Report an issue: GitHub.