D4Vinci/Scrapling · error · TypeError
Can't pickle Selector objects
Error message
Can't pickle Selector objects
What it means
Raised by Selector.__getstate__ whenever something attempts to pickle a Selector (pickle.dump, copy.deepcopy, multiprocessing queues, joblib, cloudpickle). The underlying lxml HtmlElement tree cannot be serialized, so the class deliberately fails fast with a TypeError instead of producing a corrupt pickle.
Source
Thrown at scrapling/parser.py:252
adaptive=adaptive,
_storage=storage,
keep_comments=comments,
keep_cdata=cdata,
huge_tree=huge_tree,
)
for el in elements
)
def __handle_elements(self, result: List[HtmlElement | _ElementUnicodeResult]) -> "Selectors":
"""Used internally in all functions to convert results to Selectors in bulk"""
if not result:
return Selectors()
return self.__elements_convertor(result)
def __getstate__(self) -> Any:
# lxml don't like it :)
raise TypeError("Can't pickle Selector objects")
# The following four properties I made them into functions instead of variables directly
# So they don't slow down the process of initializing many instances of the class and gets executed only
# when the user needs them for the first time for that specific element and gets cached for next times
# Doing that only made the library performance test sky rocked multiple times faster than before
# because I was executing them on initialization before :))
@property
def tag(self) -> str:
"""Get the tag name of the element"""
if self._is_text_node(self._root):
return "#text"
if not self.__tag:
self.__tag = str(self._root.tag)
return self.__tag or ""
@property
def text(self) -> TextHandler:
"""Get text content of the element"""View on GitHub (pinned to 5d213a2d47)
Solutions
- Extract plain data (strings, dicts) from the Selector before pickling, e.g. store element.html, element.attrib, or text content instead of the Selector itself.
- If multiprocessing is only used for CPU parallelism, switch to threading/asyncio since parsing is C-bound (lxml releases the GIL) and objects never need to cross process boundaries.
- As a last resort for process transfer, transfer the raw HTML bytes and re-parse with Selector(content) in the worker.
Example fix
// before
import pickle
pickle.dumps(page.css_first('h1')) # TypeError
// after
import pickle
data = page.css_first('h1').html # str is picklable
pickle.dumps(data) Defensive patterns
Strategy: validation
Validate before calling
def picklable_extraction(sel):
# reduce to primitives before any pickle/deepcopy boundary
return {
'tag': sel.tag,
'text': sel.text,
'attrib': dict(sel.attrib),
'html': sel.html,
}
results = [picklable_extraction(s) for s in page.css('a')]
pickle.dumps(results) # safe Type guard
from scrapling.parser import Selector
def is_selector(obj) -> bool:
# true means: do NOT pickle obj directly; extract data first
return isinstance(obj, Selector) Try / catch
try:
pickle.dumps(obj)
except TypeError as e:
if "Can't pickle Selector" in str(e):
raise ValueError('Extract primitive data from Selector before pickling') from e
raise Prevention
- Never pass Selector objects across multiprocessing boundaries; ship raw HTML or extracted primitives.
- Strip Selectors out of any object you intend to cache with pickle/joblib before storing.
- Keep a project rule: parsing happens in-process, only str/dict/bytes cross process or cache boundaries.
When it happens
Trigger: Calling pickle.dumps(selector), copy.deepcopy(selector), passing Selector objects between multiprocessing.Process workers, or using libraries like joblib/pandas that pickle their inputs.
Common situations: Scaling a scraper with multiprocessing and sending Selector results through a Pool; caching parsed pages with pickle; deepcopy of a dict that contains Selector objects.
Related errors
AI-assisted analysis of D4Vinci/Scrapling@5d213a2d47 (2026-08-14).
Data as JSON: /api/errors/4fe6e7631aa6b3b8.
Report an issue: GitHub.