https://github.com/python/cpython/commit/7b12a415c85d88eeb22a806b2bc9472d471b8e60
commit: 7b12a415c85d88eeb22a806b2bc9472d471b8e60
branch: main
author: sobolevn <[email protected]>
committer: sobolevn <[email protected]>
date: 2026-10-25T20:35:59Z
summary:

gh-152631: Fix `ast.parse` on lazy imports with older `feature_version` 
(#152632)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
M Grammar/python.gram
M Lib/test/test_ast/test_ast.py
M Parser/parser.c

diff --git a/Grammar/python.gram b/Grammar/python.gram
index f1800ca0597e77..a3312753f5bde8 100644
--- a/Grammar/python.gram
+++ b/Grammar/python.gram
@@ -218,15 +218,25 @@ import_stmt[stmt_ty](memo):
 # -----------------
 
 import_name[stmt_ty]:
-    | lazy="lazy"? 'import' a=dotted_as_names { _PyAST_Import(a, lazy ? 1 : 0, 
EXTRA) }
+    | "lazy" 'import' a=dotted_as_names {
+        CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+            _PyAST_Import(a, 1, EXTRA)) }
+    | 'import' a=dotted_as_names { _PyAST_Import(a, 0, EXTRA) }
 
 # note below: the ('.' | '...') is necessary because '...' is tokenized as 
ELLIPSIS
 import_from[stmt_ty]:
     | invalid_import_from
-    | lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' 
c=import_from_targets {
-        _PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA) }
-    | lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
-        _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0, 
EXTRA) }
+    | lazy="lazy" 'from' a=('.' | '...')* b=dotted_name 'import' 
c=import_from_targets {
+        CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+            _PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA)) }
+    | "lazy" 'from' a=('.' | '...')+ 'import' b=import_from_targets {
+        CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+            _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 1, EXTRA)) }
+    | 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
+        _PyPegen_checked_from_import(p, a, b, c, NULL, EXTRA) }
+    | 'from' a=('.' | '...')+ 'import' b=import_from_targets {
+        _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 0, EXTRA) }
+
 import_from_targets[asdl_alias_seq*]:
     | '(' a=import_from_as_names [','] ')' { a }
     | import_from_as_names !','
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index 08d4cfb9ae6c8a..095bc5832f6aca 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -1031,6 +1031,19 @@ def test_type_params_default_feature_version(self):
                 with self.assertRaises(SyntaxError):
                     ast.parse(sample, feature_version=(3, 12))
 
+    def test_lazy_imports_feature_version(self):
+        samples = [
+            "lazy import os",
+            "lazy from .. import name",
+            "lazy from mod import some",
+        ]
+        for sample in samples:
+            with self.subTest(sample):
+                ast.parse(sample)
+                ast.parse(sample, feature_version=(3, 15))
+                with self.assertRaises(SyntaxError):
+                    ast.parse(sample, feature_version=(3, 14))
+
     def test_invalid_major_feature_version(self):
         with self.assertRaises(ValueError):
             ast.parse('pass', feature_version=(2, 7))
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
new file mode 100644
index 00000000000000..51208108f10647
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
@@ -0,0 +1,2 @@
+Fix :func:`ast.parse` ability to parse lazy imports with *feature_version*
+older than 3.15, now it properly raises :exc:`SyntaxError`.
diff --git a/Parser/parser.c b/Parser/parser.c
index fd94f979d0e868..2300314a77888f 100644
--- a/Parser/parser.c
+++ b/Parser/parser.c
@@ -3485,7 +3485,7 @@ import_stmt_rule(Parser *p)
     return _res;
 }
 
-// import_name: "lazy"? 'import' dotted_as_names
+// import_name: "lazy" 'import' dotted_as_names | 'import' dotted_as_names
 static stmt_ty
 import_name_rule(Parser *p)
 {
@@ -3507,24 +3507,60 @@ import_name_rule(Parser *p)
     UNUSED(_start_lineno); // Only used by EXTRA macro
     int _start_col_offset = p->tokens[_mark]->col_offset;
     UNUSED(_start_col_offset); // Only used by EXTRA macro
-    { // "lazy"? 'import' dotted_as_names
+    { // "lazy" 'import' dotted_as_names
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
-        Token * _keyword;
+        D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\" 'import' dotted_as_names"));
+        expr_ty _keyword;
+        Token * _keyword_1;
         asdl_alias_seq* a;
-        void *lazy;
         if (
-            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), 
!p->error_indicator)  // "lazy"?
+            (_keyword = _PyPegen_expect_soft_keyword(p, "lazy"))  // 
soft_keyword='"lazy"'
             &&
+            (_keyword_1 = _PyPegen_expect_token(p, 500))  // token='import'
+            &&
+            (a = dotted_as_names_rule(p))  // dotted_as_names
+        )
+        {
+            D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\" 'import' dotted_as_names"));
+            Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+            if (_token == NULL) {
+                p->level--;
+                return NULL;
+            }
+            int _end_lineno = _token->end_lineno;
+            UNUSED(_end_lineno); // Only used by EXTRA macro
+            int _end_col_offset = _token->end_col_offset;
+            UNUSED(_end_col_offset); // Only used by EXTRA macro
+            _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" , 
_PyAST_Import ( a , 1 , EXTRA ) );
+            if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+                p->error_indicator = 1;
+                p->level--;
+                return NULL;
+            }
+            goto done;
+        }
+        p->mark = _mark;
+        D(fprintf(stderr, "%*c%s import_name[%d-%d]: %s failed!\n", p->level, 
' ',
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\" 'import' dotted_as_names"));
+    }
+    { // 'import' dotted_as_names
+        if (p->error_indicator) {
+            p->level--;
+            return NULL;
+        }
+        D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "'import' dotted_as_names"));
+        Token * _keyword;
+        asdl_alias_seq* a;
+        if (
             (_keyword = _PyPegen_expect_token(p, 500))  // token='import'
             &&
             (a = dotted_as_names_rule(p))  // dotted_as_names
         )
         {
-            D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
+            D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "'import' dotted_as_names"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3534,7 +3570,7 @@ import_name_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyAST_Import ( a , lazy ? 1 : 0 , EXTRA );
+            _res = _PyAST_Import ( a , 0 , EXTRA );
             if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3544,7 +3580,7 @@ import_name_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_name[%d-%d]: %s failed!\n", p->level, 
' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\"? 'import' dotted_as_names"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"'import' dotted_as_names"));
     }
     _res = NULL;
   done:
@@ -3554,8 +3590,10 @@ import_name_rule(Parser *p)
 
 // import_from:
 //     | invalid_import_from
-//     | "lazy"? 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets
-//     | "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
+//     | "lazy" 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets
+//     | "lazy" 'from' (('.' | '...'))+ 'import' import_from_targets
+//     | 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+//     | 'from' (('.' | '...'))+ 'import' import_from_targets
 static stmt_ty
 import_from_rule(Parser *p)
 {
@@ -3596,20 +3634,20 @@ import_from_rule(Parser *p)
         D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, 
' ',
                   p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"invalid_import_from"));
     }
-    { // "lazy"? 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets
+    { // "lazy" 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets"));
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\" 'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets"));
         Token * _keyword;
         Token * _keyword_1;
         asdl_seq * a;
         expr_ty b;
         asdl_alias_seq* c;
-        void *lazy;
+        expr_ty lazy;
         if (
-            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), 
!p->error_indicator)  // "lazy"?
+            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"))  // 
soft_keyword='"lazy"'
             &&
             (_keyword = _PyPegen_expect_token(p, 501))  // token='from'
             &&
@@ -3622,7 +3660,7 @@ import_from_rule(Parser *p)
             (c = import_from_targets_rule(p))  // import_from_targets
         )
         {
-            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 
'import' import_from_targets"));
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\" 'from' (('.' | '...'))* dotted_name 
'import' import_from_targets"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3632,7 +3670,7 @@ import_from_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyPegen_checked_from_import ( p , a , b , c , lazy , EXTRA 
);
+            _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" , 
_PyPegen_checked_from_import ( p , a , b , c , lazy , EXTRA ) );
             if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3642,22 +3680,109 @@ import_from_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, 
' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\" 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
     }
-    { // "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
+    { // "lazy" 'from' (('.' | '...'))+ 'import' import_from_targets
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import' 
import_from_targets"));
-        Token * _keyword;
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "\"lazy\" 'from' (('.' | '...'))+ 'import' 
import_from_targets"));
+        expr_ty _keyword;
         Token * _keyword_1;
+        Token * _keyword_2;
         asdl_seq * a;
         asdl_alias_seq* b;
-        void *lazy;
         if (
-            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), 
!p->error_indicator)  // "lazy"?
+            (_keyword = _PyPegen_expect_soft_keyword(p, "lazy"))  // 
soft_keyword='"lazy"'
+            &&
+            (_keyword_1 = _PyPegen_expect_token(p, 501))  // token='from'
+            &&
+            (a = _loop1_18_rule(p))  // (('.' | '...'))+
+            &&
+            (_keyword_2 = _PyPegen_expect_token(p, 500))  // token='import'
             &&
+            (b = import_from_targets_rule(p))  // import_from_targets
+        )
+        {
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\" 'from' (('.' | '...'))+ 'import' 
import_from_targets"));
+            Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+            if (_token == NULL) {
+                p->level--;
+                return NULL;
+            }
+            int _end_lineno = _token->end_lineno;
+            UNUSED(_end_lineno); // Only used by EXTRA macro
+            int _end_col_offset = _token->end_col_offset;
+            UNUSED(_end_col_offset); // Only used by EXTRA macro
+            _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" , 
_PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a ) , 1 , EXTRA ) );
+            if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+                p->error_indicator = 1;
+                p->level--;
+                return NULL;
+            }
+            goto done;
+        }
+        p->mark = _mark;
+        D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, 
' ',
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\" 'from' (('.' | '...'))+ 'import' import_from_targets"));
+    }
+    { // 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+        if (p->error_indicator) {
+            p->level--;
+            return NULL;
+        }
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets"));
+        Token * _keyword;
+        Token * _keyword_1;
+        asdl_seq * a;
+        expr_ty b;
+        asdl_alias_seq* c;
+        if (
+            (_keyword = _PyPegen_expect_token(p, 501))  // token='from'
+            &&
+            (a = _loop0_17_rule(p))  // (('.' | '...'))*
+            &&
+            (b = dotted_name_rule(p))  // dotted_name
+            &&
+            (_keyword_1 = _PyPegen_expect_token(p, 500))  // token='import'
+            &&
+            (c = import_from_targets_rule(p))  // import_from_targets
+        )
+        {
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import' 
import_from_targets"));
+            Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+            if (_token == NULL) {
+                p->level--;
+                return NULL;
+            }
+            int _end_lineno = _token->end_lineno;
+            UNUSED(_end_lineno); // Only used by EXTRA macro
+            int _end_col_offset = _token->end_col_offset;
+            UNUSED(_end_col_offset); // Only used by EXTRA macro
+            _res = _PyPegen_checked_from_import ( p , a , b , c , NULL , EXTRA 
);
+            if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+                p->error_indicator = 1;
+                p->level--;
+                return NULL;
+            }
+            goto done;
+        }
+        p->mark = _mark;
+        D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, 
' ',
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from' 
(('.' | '...'))* dotted_name 'import' import_from_targets"));
+    }
+    { // 'from' (('.' | '...'))+ 'import' import_from_targets
+        if (p->error_indicator) {
+            p->level--;
+            return NULL;
+        }
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', 
_mark, p->mark, "'from' (('.' | '...'))+ 'import' import_from_targets"));
+        Token * _keyword;
+        Token * _keyword_1;
+        asdl_seq * a;
+        asdl_alias_seq* b;
+        if (
             (_keyword = _PyPegen_expect_token(p, 501))  // token='from'
             &&
             (a = _loop1_18_rule(p))  // (('.' | '...'))+
@@ -3667,7 +3792,7 @@ import_from_rule(Parser *p)
             (b = import_from_targets_rule(p))  // import_from_targets
         )
         {
-            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import' 
import_from_targets"));
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))+ 'import' 
import_from_targets"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3677,7 +3802,7 @@ import_from_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a 
) , lazy ? 1 : 0 , EXTRA );
+            _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a 
) , 0 , EXTRA );
             if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3687,7 +3812,7 @@ import_from_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, 
' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, 
"\"lazy\"? 'from' (('.' | '...'))+ 'import' import_from_targets"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from' 
(('.' | '...'))+ 'import' import_from_targets"));
     }
     _res = NULL;
   done:

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to