@@ -112,25 +112,30 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
112112 if isinstance (defn .analyzed , TypedDictExpr ):
113113 existing_info = defn .analyzed .info
114114
115- base_fullname : str | None = None
116- if (
117- len (defn .base_type_exprs ) == 1
118- and isinstance (defn .base_type_exprs [0 ], RefExpr )
119- and defn .base_type_exprs [0 ].fullname in TPDICT_NAMES
120- ):
121- base_fullname = defn .base_type_exprs [0 ].fullname
122-
123115 is_closed : bool | None = None
124116 if "closed" in defn .keywords :
125- is_closed = self . parse_typeddict_closed_argument (
126- defn .keywords ["closed" ], base_fullname
117+ is_closed = require_bool_literal_argument (
118+ self . api , defn .keywords ["closed" ], "closed" , False
127119 )
128120
129121 if (
130122 len (defn .base_type_exprs ) == 1
131123 and isinstance (defn .base_type_exprs [0 ], RefExpr )
132124 and defn .base_type_exprs [0 ].fullname in TPDICT_NAMES
133125 ):
126+ if (
127+ is_closed is not None
128+ and parse_bool (defn .keywords ["closed" ]) is not None
129+ and defn .base_type_exprs [0 ].fullname == "typing.TypedDict"
130+ and self .options .python_version < (3 , 15 )
131+ and not self .api .is_stub_file
132+ and not defn .is_mypy_only
133+ ):
134+ self .fail (
135+ '"closed" argument to TypedDict is only available in Python 3.15 and later' ,
136+ defn .keywords ["closed" ],
137+ )
138+ is_closed = None
134139 # Building a new TypedDict
135140 field_sources , statements = self .analyze_typeddict_classdef_fields (defn )
136141 if field_sources is None :
@@ -157,7 +162,9 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
157162 typeddict_bases : list [Expression ] = []
158163 typeddict_bases_set = set ()
159164 for i , expr in enumerate (defn .base_type_exprs ):
160- ok , maybe_type_info , _ = self .check_typeddict (expr , inline_base (defn .name , i ))
165+ ok , maybe_type_info , _ = self .check_typeddict (
166+ expr , inline_base (defn .name , i ), defn .is_mypy_only
167+ )
161168 if ok and maybe_type_info is not None :
162169 # expr is a CallExpr
163170 info = maybe_type_info
@@ -585,7 +592,7 @@ def extract_meta_info(
585592 return typ , is_required , readonly
586593
587594 def check_typeddict (
588- self , node : Expression , name : str
595+ self , node : Expression , name : str , is_mypy_only : bool = False
589596 ) -> tuple [bool , TypeInfo | None , list [TypeVarLikeType ]]:
590597 """Check if a call defines a TypedDict.
591598
@@ -608,7 +615,7 @@ def check_typeddict(
608615 fullname = callee .fullname
609616 if fullname not in TPDICT_NAMES :
610617 return False , None , []
611- res = self .parse_typeddict_args (call , fullname )
618+ res = self .parse_typeddict_args (call , fullname , is_mypy_only )
612619 if res is None :
613620 # This is a valid typed dict, but some type is not ready.
614621 # The caller should defer this until next iteration.
@@ -671,7 +678,7 @@ def check_typeddict(
671678 return True , info , tvar_defs
672679
673680 def parse_typeddict_args (
674- self , call : CallExpr , fullname : str
681+ self , call : CallExpr , fullname : str , is_mypy_only : bool
675682 ) -> tuple [str , list [str ], list [Type ], bool , bool , list [TypeVarLikeType ], bool ] | None :
676683 """Parse typed dict call expression.
677684
@@ -712,7 +719,19 @@ def parse_typeddict_args(
712719 for arg_name , arg in zip (call .arg_names [2 :], call .args [2 :]):
713720 assert arg_name
714721 if arg_name == "closed" :
715- value = self .parse_typeddict_closed_argument (arg , fullname )
722+ value = require_bool_literal_argument (self .api , arg , "closed" , False )
723+ if (
724+ parse_bool (arg ) is not None
725+ and fullname == "typing.TypedDict"
726+ and self .options .python_version < (3 , 15 )
727+ and not self .api .is_stub_file
728+ and not is_mypy_only
729+ ):
730+ self .fail (
731+ '"closed" argument to TypedDict is only available in Python 3.15 and later' ,
732+ arg ,
733+ )
734+ return "" , [], [], True , False , [], False
716735 else :
717736 value = require_bool_literal_argument (self .api , arg , arg_name )
718737 if value is None :
@@ -731,22 +750,6 @@ def parse_typeddict_args(
731750 assert total is not None
732751 return args [0 ].value , items , types , total , closed , tvar_defs , ok
733752
734- def parse_typeddict_closed_argument (
735- self , arg : Expression , fullname : str | None
736- ) -> bool | None :
737- literal_value = parse_bool (arg )
738- value = require_bool_literal_argument (self .api , arg , "closed" , False )
739- if (
740- literal_value is not None
741- and fullname == "typing.TypedDict"
742- and self .options .python_version < (3 , 15 )
743- ):
744- self .fail (
745- '"closed" argument to TypedDict is only available in Python 3.15 and later' , arg
746- )
747- return None
748- return value
749-
750753 def parse_typeddict_fields_with_types (
751754 self , dict_items : list [tuple [Expression | None , Expression ]]
752755 ) -> tuple [list [str ], list [Type ], bool ] | None :
0 commit comments