D4Vinci/Scrapling · error · TypeError

Text nodes do not have attributes

Error message

Text nodes do not have attributes

What it means

TypeError from Selector.__getitem__: you used item access (selector['attr']) on a Selector wrapping a text node rather than an element. Text nodes (the string results of XPath like /text()) have no attributes, so attribute lookup is refused with this message. The companion __contains__ simply returns False for text nodes instead of raising.

Source

Thrown at scrapling/parser.py:185

                self._storage = _storage
            else:
                if not storage_args:
                    storage_args = {
                        "storage_file": __DEFAULT_DB_FILE__,
                        "url": url,
                    }

                if not hasattr(storage, "__wrapped__"):
                    raise ValueError("Storage class must be wrapped with lru_cache decorator, see docs for info")

                if not issubclass(storage.__wrapped__, StorageSystemMixin):  # pragma: no cover
                    raise ValueError("Storage system must be inherited from class `StorageSystemMixin`")

                self._storage = storage(**storage_args)

    def __getitem__(self, key: str) -> TextHandler:
        if self._is_text_node(self._root):
            raise TypeError("Text nodes do not have attributes")
        return self.attrib[key]

    def __contains__(self, key: str) -> bool:
        if self._is_text_node(self._root):
            return False
        return key in self.attrib

    # Node functionalities, I wanted to move to a separate Mixin class, but it had a slight impact on performance
    @staticmethod
    def _is_text_node(
        element: HtmlElement | _ElementUnicodeResult,
    ) -> bool:
        """Return True if the given element is a result of a string expression
        Examples:
            XPath -> '/text()', '/@attribute', etc...
            CSS3 -> '::text', '::attr(attrib)'...
        """
        # Faster than checking `element.is_attribute or element.is_text or element.is_tail`

View on GitHub (pinned to 5d213a2d47)

Solutions

  1. Query the element itself, then take text separately: el = page.css('h1')[0]; el['class']; text = page.css('h1::text').get().
  2. Before attribute access, check the node type: if not sel._is_text_node(sel._root): ... or guard with `if not sel.get('class') is None` style APIs on element selectors.
  3. Filter mixed results to elements first (e.g. skip results where .tag is None for text nodes).

Example fix

# before
node = page.xpath('//h1/text()').get_first()
value = node['class']  # TypeError

# after
el = page.css('h1')[0]
value = el['class']
text = page.css('h1::text').get()
Defensive patterns

Strategy: type-guard

Validate before calling

def is_element_selector(sel) -> bool:
    return not sel._is_text_node(sel._root)

node = page.xpath('//h1/text()').get_first()
if is_element_selector(node):
    value = node['class']
else:
    value = None  # text node: no attributes

Type guard

from lxml.etree import _Element

def is_element_result(sel) -> bool:
    """True when the Selector wraps an element (has attributes), not a text node."""
    from lxml.etree import _ElementUnicodeResult
    return not isinstance(sel._root, _ElementUnicodeResult)

Try / catch

try:
    value = node['class']
except TypeError as e:
    if "Text nodes do not have attributes" in str(e):
        value = None  # text node: skip attribute extraction
    else:
        raise

Prevention

When it happens

Trigger: node = selector.xpath('//h1/text()')[0]; node['href'] — the selector wraps the text string, not the h1 element. Also element.css('::text').first['class'], or iterating results where some items are text nodes and treating them uniformly as elements.

Common situations: Applying element-style code to the output of /text() or ::text pseudo-element queries; generic attribute-copy loops over mixed node lists; refactoring css selectors and accidentally appending ::text.

Related errors


AI-assisted analysis of D4Vinci/Scrapling@5d213a2d47 (2026-08-14). Data as JSON: /api/errors/354bd683faa81ba5. Report an issue: GitHub.