8686 'TYPE_CHECKING' ,
8787 'Never' ,
8888 'NoReturn' ,
89+ 'ReadOnly' ,
8990 'Required' ,
9091 'NotRequired' ,
9192
@@ -773,7 +774,7 @@ def inner(func):
773774 return inner
774775
775776
776- if sys . version_info >= ( 3 , 13 ):
777+ if hasattr ( typing , "ReadOnly" ):
777778 # The standard library TypedDict in Python 3.8 does not store runtime information
778779 # about which (if any) keys are optional. See https://bugs.python.org/issue38834
779780 # The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
@@ -784,15 +785,37 @@ def inner(func):
784785 # Aaaand on 3.12 we add __orig_bases__ to TypedDict
785786 # to enable better runtime introspection.
786787 # On 3.13 we deprecate some odd ways of creating TypedDicts.
788+ # PEP 705 proposes adding the ReadOnly[] qualifier.
787789 TypedDict = typing .TypedDict
788790 _TypedDictMeta = typing ._TypedDictMeta
789791 is_typeddict = typing .is_typeddict
790792else :
791793 # 3.10.0 and later
792794 _TAKES_MODULE = "module" in inspect .signature (typing ._type_check ).parameters
793795
796+ def _get_typeddict_qualifiers (annotation_type ):
797+ while True :
798+ annotation_origin = get_origin (annotation_type )
799+ if annotation_origin is Annotated :
800+ annotation_args = get_args (annotation_type )
801+ if annotation_args :
802+ annotation_type = annotation_args [0 ]
803+ else :
804+ break
805+ elif annotation_origin is Required :
806+ yield Required
807+ annotation_type , = get_args (annotation_type )
808+ elif annotation_origin is NotRequired :
809+ yield NotRequired
810+ annotation_type , = get_args (annotation_type )
811+ elif annotation_origin is ReadOnly :
812+ yield ReadOnly
813+ annotation_type , = get_args (annotation_type )
814+ else :
815+ break
816+
794817 class _TypedDictMeta (type ):
795- def __new__ (cls , name , bases , ns , total = True ):
818+ def __new__ (cls , name , bases , ns , * , total = True ):
796819 """Create new typed dict class object.
797820
798821 This method is called when TypedDict is subclassed,
@@ -835,33 +858,46 @@ def __new__(cls, name, bases, ns, total=True):
835858 }
836859 required_keys = set ()
837860 optional_keys = set ()
861+ readonly_keys = set ()
862+ mutable_keys = set ()
838863
839864 for base in bases :
840- annotations .update (base .__dict__ .get ('__annotations__' , {}))
841- required_keys .update (base .__dict__ .get ('__required_keys__' , ()))
842- optional_keys .update (base .__dict__ .get ('__optional_keys__' , ()))
865+ base_dict = base .__dict__
866+
867+ annotations .update (base_dict .get ('__annotations__' , {}))
868+ required_keys .update (base_dict .get ('__required_keys__' , ()))
869+ optional_keys .update (base_dict .get ('__optional_keys__' , ()))
870+ readonly_keys .update (base_dict .get ('__readonly_keys__' , ()))
871+ mutable_keys .update (base_dict .get ('__mutable_keys__' , ()))
843872
844873 annotations .update (own_annotations )
845874 for annotation_key , annotation_type in own_annotations .items ():
846- annotation_origin = get_origin (annotation_type )
847- if annotation_origin is Annotated :
848- annotation_args = get_args (annotation_type )
849- if annotation_args :
850- annotation_type = annotation_args [0 ]
851- annotation_origin = get_origin (annotation_type )
852-
853- if annotation_origin is Required :
875+ qualifiers = set (_get_typeddict_qualifiers (annotation_type ))
876+
877+ if Required in qualifiers :
854878 required_keys .add (annotation_key )
855- elif annotation_origin is NotRequired :
879+ elif NotRequired in qualifiers :
856880 optional_keys .add (annotation_key )
857881 elif total :
858882 required_keys .add (annotation_key )
859883 else :
860884 optional_keys .add (annotation_key )
885+ if ReadOnly in qualifiers :
886+ if annotation_key in mutable_keys :
887+ raise TypeError (
888+ f"Cannot override mutable key { annotation_key !r} "
889+ " with read-only key"
890+ )
891+ readonly_keys .add (annotation_key )
892+ else :
893+ mutable_keys .add (annotation_key )
894+ readonly_keys .discard (annotation_key )
861895
862896 tp_dict .__annotations__ = annotations
863897 tp_dict .__required_keys__ = frozenset (required_keys )
864898 tp_dict .__optional_keys__ = frozenset (optional_keys )
899+ tp_dict .__readonly_keys__ = frozenset (readonly_keys )
900+ tp_dict .__mutable_keys__ = frozenset (mutable_keys )
865901 if not hasattr (tp_dict , '__total__' ):
866902 tp_dict .__total__ = total
867903 return tp_dict
@@ -942,6 +978,8 @@ class Point2D(TypedDict):
942978 raise TypeError ("TypedDict takes either a dict or keyword arguments,"
943979 " but not both" )
944980 if kwargs :
981+ if sys .version_info >= (3 , 13 ):
982+ raise TypeError ("TypedDict takes no keyword arguments" )
945983 warnings .warn (
946984 "The kwargs-based syntax for TypedDict definitions is deprecated "
947985 "in Python 3.11, will be removed in Python 3.13, and may not be "
@@ -1930,6 +1968,53 @@ class Movie(TypedDict):
19301968 """ )
19311969
19321970
1971+ if hasattr (typing , 'ReadOnly' ):
1972+ ReadOnly = typing .ReadOnly
1973+ elif sys .version_info [:2 ] >= (3 , 9 ): # 3.9-3.12
1974+ @_ExtensionsSpecialForm
1975+ def ReadOnly (self , parameters ):
1976+ """A special typing construct to mark an item of a TypedDict as read-only.
1977+
1978+ For example:
1979+
1980+ class Movie(TypedDict):
1981+ title: ReadOnly[str]
1982+ year: int
1983+
1984+ def mutate_movie(m: Movie) -> None:
1985+ m["year"] = 1992 # allowed
1986+ m["title"] = "The Matrix" # typechecker error
1987+
1988+ There is no runtime checking for this property.
1989+ """
1990+ item = typing ._type_check (parameters , f'{ self ._name } accepts only a single type.' )
1991+ return typing ._GenericAlias (self , (item ,))
1992+
1993+ else : # 3.8
1994+ class _ReadOnlyForm (_ExtensionsSpecialForm , _root = True ):
1995+ def __getitem__ (self , parameters ):
1996+ item = typing ._type_check (parameters ,
1997+ f'{ self ._name } accepts only a single type.' )
1998+ return typing ._GenericAlias (self , (item ,))
1999+
2000+ ReadOnly = _ReadOnlyForm (
2001+ 'ReadOnly' ,
2002+ doc = """A special typing construct to mark a key of a TypedDict as read-only.
2003+
2004+ For example:
2005+
2006+ class Movie(TypedDict):
2007+ title: ReadOnly[str]
2008+ year: int
2009+
2010+ def mutate_movie(m: Movie) -> None:
2011+ m["year"] = 1992 # allowed
2012+ m["title"] = "The Matrix" # typechecker error
2013+
2014+ There is no runtime checking for this propery.
2015+ """ )
2016+
2017+
19332018_UNPACK_DOC = """\
19342019 Type unpack operator.
19352020
0 commit comments