Repository: huggingface/smolagents

Issue:
[BUG] Minor issue with double assign
Hello,

I am trying to use agents on scientific problems and sometimes Qwen  model outputs a weird, but a valid code like `a = b = 1`, and it fails in the executor with the following exception:
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File [~/2025/myrepos/smolagents/src/smolagents/local_python_executor.py:1392](http://localhost:8888/lab/workspaces/auto-N/tree/2025/work/2025/myrepos/smolagents/src/smolagents/local_python_executor.py#line=1391), in evaluate_python_code(code, static_tools, custom_tools, state, authorized_imports, max_print_outputs_length)
   1391 for node in expression.body:
-> 1392     result = evaluate_ast(node, state, static_tools, custom_tools, authorized_imports)
   1393 state["_print_outputs"].value = truncate_content(
   1394     str(state["_print_outputs"]), max_length=max_print_outputs_length
   1395 )

File [~/2025/myrepos/smolagents/src/smolagents/local_python_executor.py:1216](http://localhost:8888/lab/workspaces/auto-N/tree/2025/work/2025/myrepos/smolagents/src/smolagents/local_python_executor.py#line=1215), in evaluate_ast(expression, state, static_tools, custom_tools, authorized_imports)
   1213 if isinstance(expression, ast.Assign):
   1214     # Assignment -> we evaluate the assignment which should update the state
   1215     # We return the variable assigned as it may be used to determine the final result.
-> 1216     return evaluate_assign(expression, *common_params)
   1217 elif isinstance(expression, ast.AugAssign):

File [~/2025/myrepos/smolagents/src/smolagents/local_python_executor.py:535](http://localhost:8888/lab/workspaces/auto-N/tree/2025/work/2025/myrepos/smolagents/src/smolagents/local_python_executor.py#line=534), in evaluate_assign(assign, state, static_tools, custom_tools, authorized_imports)
    534 else:
--> 535     if len(assign.targets) != len(result):
    536         raise InterpreterError(f"Assign failed: expected {len(result)} values but got {len(assign.targets)}.")

TypeError: object of type 'int' has no len()

During handling of the above exception, another exception occurred:

InterpreterError                          Traceback (most recent call last)
Cell In[6], line 3
      1 import smolagents
      2 inter = smolagents.local_python_executor.LocalPythonExecutor(additional_authorized_imports=[])
----> 3 inter('a = b = 1')

File [~/2025/myrepos/smolagents/src/smolagents/local_python_executor.py:1434](http://localhost:8888/lab/workspaces/auto-N/tree/2025/work/2025/myrepos/smolagents/src/smolagents/local_python_executor.py#line=1433), in LocalPythonExecutor.__call__(self, code_action)
   1433 def __call__(self, code_action: str) -> Tuple[Any, str, bool]:
-> 1434     output, is_final_answer = evaluate_python_code(
   1435         code_action,
   1436         static_tools=self.static_tools,
   1437         custom_tools=self.custom_tools,
   1438         state=self.state,
   1439         authorized_imports=self.authorized_imports,
   1440         max_print_outputs_length=self.max_print_outputs_length,
   1441     )
   1442     logs = str(self.state["_print_outputs"])
   1443     return output, logs, is_final_answer

File [~/2025/myrepos/smolagents/src/smolagents/local_python_executor.py:1408](http://localhost:8888/lab/workspaces/auto-N/tree/2025/work/2025/myrepos/smolagents/src/smolagents/local_python_executor.py#line=1407), in evaluate_python_code(code, static_tools, custom_tools, state, authorized_imports, max_print_outputs_length)
   1404 except Exception as e:
   1405     state["_print_outputs"].value = truncate_content(
   1406         str(state["_print_outputs"]), max_length=max_print_outputs_length
   1407     )
-> 1408     raise InterpreterError(
   1409         f"Code execution failed at line '{ast.get_source_segment(code, node)}' due to: {type(e).__name__}: {e}"
   1410     )

InterpreterError: Code execution failed at line 'a = b = 1' due to: TypeError: object of type 'int' has no len()
```

**Code to reproduce the error**
The simplest code snippet that produces the bug:
```
import smolagents
inter = smolagents.local_python_executor.LocalPythonExecutor(additional_authorized_imports=[])
inter('a = b = 1')
```


**Packages version:**
 '1.9.2' and `main`

**Possible solution**
Remove if check here: 
https://github.com/huggingface/smolagents/blob/eef2c17c3ec8cb05acba3881454017014043a2f6/src/smolagents/local_python_executor.py#L535

**Expected behavior**
The code is valid and the agent should not fail

Discussion:
@sysradium: @Bilokin can you check if the following fix works for you: https://github.com/huggingface/smolagents/pull/843?

Solution patch:
diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py
index 19ad59e..ebb8a0a 100644
--- a/src/smolagents/local_python_executor.py
+++ b/src/smolagents/local_python_executor.py
@@ -590,14 +590,13 @@ def evaluate_assign(
         target = assign.targets[0]
         set_value(target, result, state, static_tools, custom_tools, authorized_imports)
     else:
-        if len(assign.targets) != len(result):
-            raise InterpreterError(f"Assign failed: expected {len(result)} values but got {len(assign.targets)}.")
         expanded_values = []
         for tgt in assign.targets:
             if isinstance(tgt, ast.Starred):
                 expanded_values.extend(result)
             else:
                 expanded_values.append(result)
+
         for tgt, val in zip(assign.targets, expanded_values):
             set_value(tgt, val, state, static_tools, custom_tools, authorized_imports)
     return result
@@ -641,17 +640,21 @@ def evaluate_call(
     custom_tools: Dict[str, Callable],
     authorized_imports: List[str],
 ) -> Any:
-    if not (
-        isinstance(call.func, ast.Attribute) or isinstance(call.func, ast.Name) or isinstance(call.func, ast.Subscript)
-    ):
+    if not isinstance(call.func, (ast.Call, ast.Lambda, ast.Attribute, ast.Name, ast.Subscript)):
         raise InterpreterError(f"This is not a correct function: {call.func}).")
-    if isinstance(call.func, ast.Attribute):
+
+    func, func_name = None, None
+
+    if isinstance(call.func, ast.Call):
+        func = evaluate_call(call.func, state, static_tools, custom_tools, authorized_imports)
+    elif isinstance(call.func, ast.Lambda):
+        func = evaluate_lambda(call.func, state, static_tools, custom_tools, authorized_imports)
+    elif isinstance(call.func, ast.Attribute):
         obj = evaluate_ast(call.func.value, state, static_tools, custom_tools, authorized_imports)
         func_name = call.func.attr
         if not hasattr(obj, func_name):
             raise InterpreterError(f"Object {obj} has no attribute {func_name}")
         func = getattr(obj, func_name)
-
     elif isinstance(call.func, ast.Name):
         func_name = call.func.id
         if func_name in state:
@@ -666,12 +669,12 @@ def evaluate_call(
             raise InterpreterError(
                 f"It is not permitted to evaluate other functions than the provided tools or functions defined/imported in previous code (tried to execute {call.func.id})."
             )
-
     elif isinstance(call.func, ast.Subscript):
         func = evaluate_subscript(call.func, state, static_tools, custom_tools, authorized_imports)
         if not callable(func):
             raise InterpreterError(f"This is not a correct function: {call.func}).")
         func_name = None
+
     args = []
     for arg in call.args:
         if isinstance(arg, ast.Starred):
@@ -700,20 +703,15 @@ def evaluate_call(
             return super(cls, instance)
         else:
             raise InterpreterError("super() takes at most 2 arguments")
-    else:
-        if func_name == "print":
-            state["_print_outputs"] += " ".join(map(str, args)) + "\n"
-            return None
-        else:  # Assume it's a callable object
-            if (
-                (inspect.getmodule(func) == builtins)
-                and inspect.isbuiltin(func)
-                and (func not in static_tools.values())
-            ):
-                raise InterpreterError(
-                    f"Invoking a builtin function that has not been explicitly added as a tool is not allowed ({func_name})."
-                )
-            return func(*args, **kwargs)
+    elif func_name == "print":
+        state["_print_outputs"] += " ".join(map(str, args)) + "\n"
+        return None
+    else:  # Assume it's a callable object
+        if (inspect.getmodule(func) == builtins) and inspect.isbuiltin(func) and (func not in static_tools.values()):
+            raise InterpreterError(
+                f"Invoking a builtin function that has not been explicitly added as a tool is not allowed ({func_name})."
+            )
+        return func(*args, **kwargs)
 
 
 def evaluate_subscript(


Test patch:
diff --git a/tests/test_local_python_executor.py b/tests/test_local_python_executor.py
index 06367f0..bb071b1 100644
--- a/tests/test_local_python_executor.py
+++ b/tests/test_local_python_executor.py
@@ -1423,6 +1423,44 @@ class TestLocalPythonExecutor:
         result, _, _ = executor(code)
         assert result == 11
 
+    @pytest.mark.parametrize(
+        "code",
+        [
+            "a = b = 1; a",
+            "a = b = 1; b",
+            "a, b = c, d = 1, 1; a",
+            "a, b = c, d = 1, 1; b",
+            "a, b = c, d = 1, 1; c",
+            "a, b = c, d = {1, 2}; a",
+            "a, b = c, d = {1, 2}; c",
+            "a, b = c, d = {1: 10, 2: 20}; a",
+            "a, b = c, d = {1: 10, 2: 20}; c",
+            "a = b = (lambda: 1)(); b",
+            "a = b = (lambda: 1)(); lambda x: 10; b",
+            "a = b = (lambda x: lambda y: x + y)(0)(1); b",
+            dedent("""
+            def foo():
+                return 1;
+            a = b = foo(); b"""),
+            dedent("""
+            def foo(*args, **kwargs):
+                return sum(args)
+            a = b = foo(1,-1,1); b"""),
+            "a, b = 1, 2; a, b = b, a; b",
+        ],
+    )
+    def test_chained_assignments(self, code):
+        executor = LocalPythonExecutor([])
+        executor.send_tools({})
+        result, _, _ = executor(code)
+        assert result == 1
+
+    def test_evaluate_assign_error(self):
+        code = "a, b = 1, 2, 3; a"
+        executor = LocalPythonExecutor([])
+        with pytest.raises(InterpreterError, match=".*Cannot unpack tuple of wrong size"):
+            executor(code)
+
 
 class TestLocalPythonExecutorSecurity:
     @pytest.mark.parametrize(