D4Vinci/Scrapling · error · TypeError

content argument must be str or bytes, got {type(content)}

Error message

content argument must be str or bytes, got {type(content)}

What it means

TypeError from Selector.__init__ when `content` is provided but is neither str nor bytes (and `root` is None). The parser only accepts string or bytes HTML bodies; any other type (None, int, dict, an etree element passed via content instead of root) is rejected.

Source

Thrown at scrapling/parser.py:139

        self.url = url
        self._raw_body: str | bytes = ""
        self.encoding = encoding
        self.__keep_cdata = keep_cdata
        self.__huge_tree_enabled = huge_tree
        self.__keep_comments = keep_comments
        # For selector stuff
        self.__text: Optional[TextHandler] = None
        self.__attributes: Optional[AttributesHandler] = None
        self.__tag: Optional[str] = None
        self._storage: Optional[StorageSystemMixin] = None
        if root is None:
            body: str | bytes
            if isinstance(content, str):
                body = content.strip().replace("\x00", "") or "<html/>"
            elif isinstance(content, bytes):
                body = content.replace(b"\x00", b"")
            else:
                raise TypeError(f"content argument must be str or bytes, got {type(content)}")

            # https://lxml.de/api/lxml.etree.HTMLParser-class.html
            _parser_kwargs: Dict[str, Any] = dict(
                recover=True,
                remove_blank_text=True,
                remove_comments=(not keep_comments),
                encoding=encoding,
                compact=True,
                huge_tree=huge_tree,
                default_doctype=True,  # Supported by lxml but missing from stubs
                strip_cdata=(not keep_cdata),
            )
            parser = HTMLParser(**_parser_kwargs)
            self._root = cast(HtmlElement, fromstring(body or "<html/>", parser=parser, base_url=url or ""))
            self._raw_body = content

        else:
            self._root = cast(HtmlElement, root)

View on GitHub (pinned to 5d213a2d47)

Solutions

  1. Pass the HTML as str or bytes: Selector(content='<div>x</div>') or Selector(content=b'<div>x</div>').
  2. For lxml elements use the root= parameter, which takes priority.
  3. Extract the right field first: Selector(content=response.content).

Example fix

# before
sel = Selector(content=response)  # Response object, not HTML

# after
sel = Selector(content=response.content)  # str/bytes HTML
Defensive patterns

Strategy: type-guard

Validate before calling

assert isinstance(content, (str, bytes)), f"content must be str/bytes, got {type(content).__name__}"
sel = Selector(content=content)

Type guard

def is_html_content(value: object) -> bool:
    return isinstance(value, (str, bytes))

Try / catch

try:
    sel = Selector(content=body)
except TypeError as e:
    if "str or bytes" in str(e):
        sel = Selector(content=str(body))  # last-resort coercion
    else:
        raise

Prevention

When it happens

Trigger: Selector(content=123), Selector(content={'html': '...'}), Selector(content=etree_element) (should use root=), or Selector(content=response) passing the whole response object.

Common situations: Passing an already-parsed lxml element through the wrong parameter; JSON APIs returning HTML nested in a dict; passing a Response object where .content was intended; numeric/None defaults leaking from config.

Related errors


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