headroomlabs-ai/headroom · error · AttributeError

module {__name__!r} has no attribute {name!r}

Error message

module {__name__!r} has no attribute {name!r}

What it means

headroom.transforms implements lazy exports via a module-level __getattr__: names listed in _LAZY_EXPORTS are imported on first access so heavy modules (e.g. the HTML extractor) load only when used. Any attribute not in globals(), __all__, or _LAZY_EXPORTS raises AttributeError with the module's __name__ and the missing name — the standard 'no attribute' semantics, just produced by the lazy loader.

Source

Thrown at headroom/transforms/__init__.py:221

    "CacheAligner": ("headroom.transforms.cache_aligner", "CacheAligner"),
    # HTML extraction (optional dependency - requires trafilatura)
    "HTMLExtractor": ("headroom.transforms.html_extractor", "HTMLExtractor"),
    "HTMLExtractorConfig": ("headroom.transforms.html_extractor", "HTMLExtractorConfig"),
    "HTMLExtractionResult": ("headroom.transforms.html_extractor", "HTMLExtractionResult"),
    "is_html_content": ("headroom.transforms.html_extractor", "is_html_content"),
}


def __getattr__(name: str) -> object:
    if name == "__path__":
        raise AttributeError(name)
    if name == "_HTML_EXTRACTOR_AVAILABLE":
        return _HTML_EXTRACTOR_AVAILABLE

    try:
        module_name, attr_name = _LAZY_EXPORTS[name]
    except KeyError as exc:
        raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc

    module = import_module(module_name)
    value = getattr(module, attr_name)
    globals()[name] = value
    return value


def __dir__() -> list[str]:
    return sorted(set(globals()) | set(__all__))

View on GitHub (pinned to 322425c43b)

Solutions

  1. Check the package __all__/docs for the exact public spelling and use it.
  2. If the symbol genuinely exists in a submodule but is not re-exported, import from the full path: from headroom.transforms.html import extract_x.
  3. On version upgrades, grep the changelog for renames of the transform you use.
  4. Use dir(headroom.transforms) — __dir__ merges globals and __all__ — to see what is addressable.

Example fix

# before
from headroom.transforms import strip_html  # AttributeError: not exported

# after
import headroom.transforms as T
print([n for n in dir(T) if "html" in n.lower()])
from headroom.transforms import HtmlStripTransform  # exact exported name
Defensive patterns

Strategy: type-guard

Validate before calling

import headroom.transforms as T
assert "TargetSymbol" in dir(T), f"not exported; available: {[n for n in dir(T)]}"

Type guard

def exports(name: str) -> bool:
    import headroom.transforms as T
    return name in dir(T)

Try / catch

try:
    from headroom.transforms import TargetSymbol
except AttributeError as e:
    raise ImportError(f"{e}; check headroom.transforms.__all__ for the public name") from e

Prevention

When it happens

Trigger: from headroom.transforms import StripHTMLTypo; transforms.SomeTransform (name not exported under that spelling); accessing a symbol that lives in a submodule directly (headroom.transforms.html.extract) but was never re-exported in _LAZY_EXPORTS/__all__.

Common situations: IDE autocompleting a private helper; names renamed between versions (old imports break); users assuming every symbol in submodules is re-exported at the package root; `import headroom.transforms as t; t.partial` style probing.

Related errors


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