https://github.com/python/cpython/commit/b723c8be071afcf3f865c55a5efb6da54f7695a0
commit: b723c8be071afcf3f865c55a5efb6da54f7695a0
branch: main
author: Krzysztof Magusiak <chris.magus...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-07-31T12:55:00+03:00
summary:

gh-124503: Optimize ast.literal_eval() for small input (GH-137010)

The implementation does not create anymore local functions which reduces
the overhead for small inputs. Some other calls are inlined into a
single `_convert_literal` function.
We have a gain of 10-20% for small inputs and only 1-2% for bigger
inputs.

files:
A Misc/NEWS.d/next/Library/2025-07-30-11-12-22.gh-issue-124503.d4hc7b.rst
M Lib/ast.py

diff --git a/Lib/ast.py b/Lib/ast.py
index 6d3daf64f5c6d7..983ac1710d0205 100644
--- a/Lib/ast.py
+++ b/Lib/ast.py
@@ -57,53 +57,60 @@ def literal_eval(node_or_string):
     Caution: A complex expression can overflow the C stack and cause a crash.
     """
     if isinstance(node_or_string, str):
-        node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval')
-    if isinstance(node_or_string, Expression):
+        node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval').body
+    elif isinstance(node_or_string, Expression):
         node_or_string = node_or_string.body
-    def _raise_malformed_node(node):
-        msg = "malformed node or string"
-        if lno := getattr(node, 'lineno', None):
-            msg += f' on line {lno}'
-        raise ValueError(msg + f': {node!r}')
-    def _convert_num(node):
-        if not isinstance(node, Constant) or type(node.value) not in (int, 
float, complex):
-            _raise_malformed_node(node)
+    return _convert_literal(node_or_string)
+
+
+def _convert_literal(node):
+    """
+    Used by `literal_eval` to convert an AST node into a value.
+    """
+    if isinstance(node, Constant):
         return node.value
-    def _convert_signed_num(node):
-        if isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)):
-            operand = _convert_num(node.operand)
-            if isinstance(node.op, UAdd):
-                return + operand
-            else:
-                return - operand
-        return _convert_num(node)
-    def _convert(node):
-        if isinstance(node, Constant):
-            return node.value
-        elif isinstance(node, Tuple):
-            return tuple(map(_convert, node.elts))
-        elif isinstance(node, List):
-            return list(map(_convert, node.elts))
-        elif isinstance(node, Set):
-            return set(map(_convert, node.elts))
-        elif (isinstance(node, Call) and isinstance(node.func, Name) and
-              node.func.id == 'set' and node.args == node.keywords == []):
-            return set()
-        elif isinstance(node, Dict):
-            if len(node.keys) != len(node.values):
-                _raise_malformed_node(node)
-            return dict(zip(map(_convert, node.keys),
-                            map(_convert, node.values)))
-        elif isinstance(node, BinOp) and isinstance(node.op, (Add, Sub)):
-            left = _convert_signed_num(node.left)
-            right = _convert_num(node.right)
-            if isinstance(left, (int, float)) and isinstance(right, complex):
-                if isinstance(node.op, Add):
-                    return left + right
-                else:
-                    return left - right
-        return _convert_signed_num(node)
-    return _convert(node_or_string)
+    if isinstance(node, Dict) and len(node.keys) == len(node.values):
+        return dict(zip(
+            map(_convert_literal, node.keys),
+            map(_convert_literal, node.values),
+        ))
+    if isinstance(node, Tuple):
+        return tuple(map(_convert_literal, node.elts))
+    if isinstance(node, List):
+        return list(map(_convert_literal, node.elts))
+    if isinstance(node, Set):
+        return set(map(_convert_literal, node.elts))
+    if (
+        isinstance(node, Call) and isinstance(node.func, Name)
+        and node.func.id == 'set' and node.args == node.keywords == []
+    ):
+        return set()
+    if (
+        isinstance(node, UnaryOp)
+        and isinstance(node.op, (UAdd, USub))
+        and isinstance(node.operand, Constant)
+        and type(operand := node.operand.value) in (int, float, complex)
+    ):
+        if isinstance(node.op, UAdd):
+            return + operand
+        else:
+            return - operand
+    if (
+        isinstance(node, BinOp)
+        and isinstance(node.op, (Add, Sub))
+        and isinstance(node.left, (Constant, UnaryOp))
+        and isinstance(node.right, Constant)
+        and type(left := _convert_literal(node.left)) in (int, float)
+        and type(right := _convert_literal(node.right)) is complex
+    ):
+        if isinstance(node.op, Add):
+            return left + right
+        else:
+            return left - right
+    msg = "malformed node or string"
+    if lno := getattr(node, 'lineno', None):
+        msg += f' on line {lno}'
+    raise ValueError(msg + f': {node!r}')
 
 
 def dump(
diff --git 
a/Misc/NEWS.d/next/Library/2025-07-30-11-12-22.gh-issue-124503.d4hc7b.rst 
b/Misc/NEWS.d/next/Library/2025-07-30-11-12-22.gh-issue-124503.d4hc7b.rst
new file mode 100644
index 00000000000000..c04eba932a0f2e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-07-30-11-12-22.gh-issue-124503.d4hc7b.rst
@@ -0,0 +1 @@
+:func:`ast.literal_eval` is 10-20% faster for small inputs.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to