144144 'override' ,
145145 'ParamSpecArgs' ,
146146 'ParamSpecKwargs' ,
147+ 'ReadOnly' ,
147148 'Required' ,
148149 'reveal_type' ,
149150 'runtime_checkable' ,
@@ -2301,7 +2302,7 @@ def _strip_annotations(t):
23012302 """Strip the annotations from a given type."""
23022303 if isinstance (t , _AnnotatedAlias ):
23032304 return _strip_annotations (t .__origin__ )
2304- if hasattr (t , "__origin__" ) and t .__origin__ in (Required , NotRequired ):
2305+ if hasattr (t , "__origin__" ) and t .__origin__ in (Required , NotRequired , ReadOnly ):
23052306 return _strip_annotations (t .__args__ [0 ])
23062307 if isinstance (t , _GenericAlias ):
23072308 stripped_args = tuple (_strip_annotations (a ) for a in t .__args__ )
@@ -2922,6 +2923,28 @@ def _namedtuple_mro_entries(bases):
29222923NamedTuple .__mro_entries__ = _namedtuple_mro_entries
29232924
29242925
2926+ def _get_typeddict_qualifiers (annotation_type ):
2927+ while True :
2928+ annotation_origin = get_origin (annotation_type )
2929+ if annotation_origin is Annotated :
2930+ annotation_args = get_args (annotation_type )
2931+ if annotation_args :
2932+ annotation_type = annotation_args [0 ]
2933+ else :
2934+ break
2935+ elif annotation_origin is Required :
2936+ yield Required
2937+ (annotation_type ,) = get_args (annotation_type )
2938+ elif annotation_origin is NotRequired :
2939+ yield NotRequired
2940+ (annotation_type ,) = get_args (annotation_type )
2941+ elif annotation_origin is ReadOnly :
2942+ yield ReadOnly
2943+ (annotation_type ,) = get_args (annotation_type )
2944+ else :
2945+ break
2946+
2947+
29252948class _TypedDictMeta (type ):
29262949 def __new__ (cls , name , bases , ns , total = True ):
29272950 """Create a new typed dict class object.
@@ -2955,6 +2978,8 @@ def __new__(cls, name, bases, ns, total=True):
29552978 }
29562979 required_keys = set ()
29572980 optional_keys = set ()
2981+ readonly_keys = set ()
2982+ mutable_keys = set ()
29582983
29592984 for base in bases :
29602985 annotations .update (base .__dict__ .get ('__annotations__' , {}))
@@ -2967,18 +2992,15 @@ def __new__(cls, name, bases, ns, total=True):
29672992 required_keys -= base_optional
29682993 optional_keys |= base_optional
29692994
2995+ readonly_keys .update (base .__dict__ .get ('__readonly_keys__' , ()))
2996+ mutable_keys .update (base .__dict__ .get ('__mutable_keys__' , ()))
2997+
29702998 annotations .update (own_annotations )
29712999 for annotation_key , annotation_type in own_annotations .items ():
2972- annotation_origin = get_origin (annotation_type )
2973- if annotation_origin is Annotated :
2974- annotation_args = get_args (annotation_type )
2975- if annotation_args :
2976- annotation_type = annotation_args [0 ]
2977- annotation_origin = get_origin (annotation_type )
2978-
2979- if annotation_origin is Required :
3000+ qualifiers = set (_get_typeddict_qualifiers (annotation_type ))
3001+ if Required in qualifiers :
29803002 is_required = True
2981- elif annotation_origin is NotRequired :
3003+ elif NotRequired in qualifiers :
29823004 is_required = False
29833005 else :
29843006 is_required = total
@@ -2990,13 +3012,26 @@ def __new__(cls, name, bases, ns, total=True):
29903012 optional_keys .add (annotation_key )
29913013 required_keys .discard (annotation_key )
29923014
3015+ if ReadOnly in qualifiers :
3016+ if annotation_key in mutable_keys :
3017+ raise TypeError (
3018+ f"Cannot override mutable key { annotation_key !r} "
3019+ " with read-only key"
3020+ )
3021+ readonly_keys .add (annotation_key )
3022+ else :
3023+ mutable_keys .add (annotation_key )
3024+ readonly_keys .discard (annotation_key )
3025+
29933026 assert required_keys .isdisjoint (optional_keys ), (
29943027 f"Required keys overlap with optional keys in { name } :"
29953028 f" { required_keys = } , { optional_keys = } "
29963029 )
29973030 tp_dict .__annotations__ = annotations
29983031 tp_dict .__required_keys__ = frozenset (required_keys )
29993032 tp_dict .__optional_keys__ = frozenset (optional_keys )
3033+ tp_dict .__readonly_keys__ = frozenset (readonly_keys )
3034+ tp_dict .__mutable_keys__ = frozenset (mutable_keys )
30003035 tp_dict .__total__ = total
30013036 return tp_dict
30023037
@@ -3055,6 +3090,14 @@ class Point2D(TypedDict):
30553090 y: NotRequired[int] # the "y" key can be omitted
30563091
30573092 See PEP 655 for more details on Required and NotRequired.
3093+
3094+ The ReadOnly special form can be used
3095+ to mark individual keys as immutable for type checkers::
3096+
3097+ class DatabaseUser(TypedDict):
3098+ id: ReadOnly[int] # the "id" key must not be modified
3099+ username: str # the "username" key can be changed
3100+
30583101 """
30593102 if fields is _sentinel or fields is None :
30603103 import warnings
@@ -3131,6 +3174,26 @@ class Movie(TypedDict):
31313174 return _GenericAlias (self , (item ,))
31323175
31333176
3177+ @_SpecialForm
3178+ def ReadOnly (self , parameters ):
3179+ """A special typing construct to mark an item of a TypedDict as read-only.
3180+
3181+ For example::
3182+
3183+ class Movie(TypedDict):
3184+ title: ReadOnly[str]
3185+ year: int
3186+
3187+ def mutate_movie(m: Movie) -> None:
3188+ m["year"] = 1992 # allowed
3189+ m["title"] = "The Matrix" # typechecker error
3190+
3191+ There is no runtime checking for this property.
3192+ """
3193+ item = _type_check (parameters , f'{ self ._name } accepts only a single type.' )
3194+ return _GenericAlias (self , (item ,))
3195+
3196+
31343197class NewType :
31353198 """NewType creates simple unique types with almost zero runtime overhead.
31363199
0 commit comments