86
86
'TYPE_CHECKING' ,
87
87
'Never' ,
88
88
'NoReturn' ,
89
+ 'ReadOnly' ,
89
90
'Required' ,
90
91
'NotRequired' ,
91
92
@@ -773,7 +774,7 @@ def inner(func):
773
774
return inner
774
775
775
776
776
- if sys . version_info >= ( 3 , 13 ):
777
+ if hasattr ( typing , "ReadOnly" ):
777
778
# The standard library TypedDict in Python 3.8 does not store runtime information
778
779
# about which (if any) keys are optional. See https://bugs.python.org/issue38834
779
780
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
@@ -784,15 +785,37 @@ def inner(func):
784
785
# Aaaand on 3.12 we add __orig_bases__ to TypedDict
785
786
# to enable better runtime introspection.
786
787
# On 3.13 we deprecate some odd ways of creating TypedDicts.
788
+ # PEP 705 proposes adding the ReadOnly[] qualifier.
787
789
TypedDict = typing .TypedDict
788
790
_TypedDictMeta = typing ._TypedDictMeta
789
791
is_typeddict = typing .is_typeddict
790
792
else :
791
793
# 3.10.0 and later
792
794
_TAKES_MODULE = "module" in inspect .signature (typing ._type_check ).parameters
793
795
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
+
794
817
class _TypedDictMeta (type ):
795
- def __new__ (cls , name , bases , ns , total = True ):
818
+ def __new__ (cls , name , bases , ns , * , total = True ):
796
819
"""Create new typed dict class object.
797
820
798
821
This method is called when TypedDict is subclassed,
@@ -835,33 +858,46 @@ def __new__(cls, name, bases, ns, total=True):
835
858
}
836
859
required_keys = set ()
837
860
optional_keys = set ()
861
+ readonly_keys = set ()
862
+ mutable_keys = set ()
838
863
839
864
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__' , ()))
843
872
844
873
annotations .update (own_annotations )
845
874
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 :
854
878
required_keys .add (annotation_key )
855
- elif annotation_origin is NotRequired :
879
+ elif NotRequired in qualifiers :
856
880
optional_keys .add (annotation_key )
857
881
elif total :
858
882
required_keys .add (annotation_key )
859
883
else :
860
884
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 )
861
895
862
896
tp_dict .__annotations__ = annotations
863
897
tp_dict .__required_keys__ = frozenset (required_keys )
864
898
tp_dict .__optional_keys__ = frozenset (optional_keys )
899
+ tp_dict .__readonly_keys__ = frozenset (readonly_keys )
900
+ tp_dict .__mutable_keys__ = frozenset (mutable_keys )
865
901
if not hasattr (tp_dict , '__total__' ):
866
902
tp_dict .__total__ = total
867
903
return tp_dict
@@ -942,6 +978,8 @@ class Point2D(TypedDict):
942
978
raise TypeError ("TypedDict takes either a dict or keyword arguments,"
943
979
" but not both" )
944
980
if kwargs :
981
+ if sys .version_info >= (3 , 13 ):
982
+ raise TypeError ("TypedDict takes no keyword arguments" )
945
983
warnings .warn (
946
984
"The kwargs-based syntax for TypedDict definitions is deprecated "
947
985
"in Python 3.11, will be removed in Python 3.13, and may not be "
@@ -1930,6 +1968,53 @@ class Movie(TypedDict):
1930
1968
""" )
1931
1969
1932
1970
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
+
1933
2018
_UNPACK_DOC = """\
1934
2019
Type unpack operator.
1935
2020
0 commit comments