From dab6222d7381967317b0a334ddb357d0ca68f700 Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Sun, 27 Aug 2023 15:41:56 +0200 Subject: [PATCH 1/2] Update specification --- pep-0692.rst | 200 ++++----------------------------------------------- 1 file changed, 13 insertions(+), 187 deletions(-) diff --git a/pep-0692.rst b/pep-0692.rst index bb12250eb98..eecb9c3b997 100644 --- a/pep-0692.rst +++ b/pep-0692.rst @@ -124,6 +124,17 @@ type ``int``). This indicates that the function should be called as follows:: foo(**kwargs) # OK! foo(name="The Meaning of Life", year=1983) # OK! +For the purposes of type checking, functions annotaded with ``Unpack`` should +be considered equivalent to their explicitly expanded counterparts. Therefore, +from the type checkers point of view:: + + foo(**kwargs: Unpack[Movie]) -> None: ... + +is equivalent to:: + + foo(*, name: str, year: int) -> None: ... + + When ``Unpack`` is used, type checkers treat ``kwargs`` inside the function body as a ``TypedDict``:: @@ -195,193 +206,8 @@ Assignment Assignments of a function typed with ``**kwargs: Unpack[Movie]`` and another callable type should pass type checking only if they are compatible. -This can happen for the scenarios described below. - -Source and destination contain ``**kwargs`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Both destination and source functions have a ``**kwargs: Unpack[TypedDict]`` -parameter and the destination function's ``TypedDict`` is assignable to the -source function's ``TypedDict`` and the rest of the parameters are -compatible:: - - class Animal(TypedDict): - name: str - - class Dog(Animal): - breed: str - - def accept_animal(**kwargs: Unpack[Animal]): ... - def accept_dog(**kwargs: Unpack[Dog]): ... - - accept_dog = accept_animal # OK! Expression of type Dog can be - # assigned to a variable of type Animal. - - accept_animal = accept_dog # WRONG! Expression of type Animal - # cannot be assigned to a variable of type Dog. - -.. _PEP 692 assignment dest no kwargs: - -Source contains ``**kwargs`` and destination doesn't -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The destination callable doesn't contain ``**kwargs``, the source callable -contains ``**kwargs: Unpack[TypedDict]`` and the destination function's keyword -arguments are assignable to the corresponding keys in source function's -``TypedDict``. Moreover, not required keys should correspond to optional -function arguments, whereas required keys should correspond to required -function arguments. Again, the rest of the parameters have to be compatible. -Continuing the previous example:: - - class Example(TypedDict): - animal: Animal - string: str - number: NotRequired[int] - - def src(**kwargs: Unpack[Example]): ... - def dest(*, animal: Dog, string: str, number: int = ...): ... - - dest = src # OK! - -It is worth pointing out that the destination function's parameters that are to -be compatible with the keys and values from the ``TypedDict`` must be keyword -only:: - - def dest(dog: Dog, string: str, number: int = ...): ... - - dog: Dog = {"name": "Daisy", "breed": "labrador"} - - dest(dog, "some string") # OK! - - dest = src # Type checker error! - dest(dog, "some string") # The same call fails at - # runtime now because 'src' expects - # keyword arguments. - -The reverse situation where the destination callable contains -``**kwargs: Unpack[TypedDict]`` and the source callable doesn't contain -``**kwargs`` should be disallowed. This is because, we cannot be sure that -additional keyword arguments are not being passed in when an instance of a -subclass had been assigned to a variable with a base class type and then -unpacked in the destination callable invocation:: - - def dest(**kwargs: Unpack[Animal]): ... - def src(name: str): ... - - dog: Dog = {"name": "Daisy", "breed": "Labrador"} - animal: Animal = dog - - dest = src # WRONG! - dest(**animal) # Fails at runtime. - -Similar situation can happen even without inheritance as compatibility -between ``TypedDict``\s is based on structural subtyping. - -Source contains untyped ``**kwargs`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The destination callable contains ``**kwargs: Unpack[TypedDict]`` and the -source callable contains untyped ``**kwargs``:: - - def src(**kwargs): ... - def dest(**kwargs: Unpack[Movie]): ... - - dest = src # OK! - -Source contains traditionally typed ``**kwargs: T`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The destination callable contains ``**kwargs: Unpack[TypedDict]``, the source -callable contains traditionally typed ``**kwargs: T`` and each of the -destination function ``TypedDict``'s fields is assignable to a variable of -type ``T``:: - - class Vehicle: - ... - - class Car(Vehicle): - ... - - class Motorcycle(Vehicle): - ... - - class Vehicles(TypedDict): - car: Car - moto: Motorcycle - - def dest(**kwargs: Unpack[Vehicles]): ... - def src(**kwargs: Vehicle): ... - - dest = src # OK! - -On the other hand, if the destination callable contains either untyped or -traditionally typed ``**kwargs: T`` and the source callable is typed using -``**kwargs: Unpack[TypedDict]`` then an error should be generated, because -traditionally typed ``**kwargs`` aren't checked for keyword names. - -To summarize, function parameters should behave contravariantly and function -return types should behave covariantly. - -Passing kwargs inside a function to another function ----------------------------------------------------- - -`A previous point `_ -mentions the problem of possibly passing additional keyword arguments by -assigning a subclass instance to a variable that has a base class type. Let's -consider the following example:: - - class Animal(TypedDict): - name: str - - class Dog(Animal): - breed: str - - def takes_name(name: str): ... - - dog: Dog = {"name": "Daisy", "breed": "Labrador"} - animal: Animal = dog - - def foo(**kwargs: Unpack[Animal]): - print(kwargs["name"].capitalize()) - - def bar(**kwargs: Unpack[Animal]): - takes_name(**kwargs) - - def baz(animal: Animal): - takes_name(**animal) - - def spam(**kwargs: Unpack[Animal]): - baz(kwargs) - - foo(**animal) # OK! foo only expects and uses keywords of 'Animal'. - - bar(**animal) # WRONG! This will fail at runtime because 'breed' keyword - # will be passed to 'takes_name' as well. - - spam(**animal) # WRONG! Again, 'breed' keyword will be eventually passed - # to 'takes_name'. - -In the example above, the call to ``foo`` will not cause any issues at -runtime. Even though ``foo`` expects ``kwargs`` of type ``Animal`` it doesn't -matter if it receives additional arguments because it only reads and uses what -it needs completely ignoring any additional values. - -The calls to ``bar`` and ``spam`` will fail because an unexpected keyword -argument will be passed to the ``takes_name`` function. - -Therefore, ``kwargs`` hinted with an unpacked ``TypedDict`` can only be passed -to another function if the function to which unpacked kwargs are being passed -to has ``**kwargs`` in its signature as well, because then additional keywords -would not cause errors at runtime during function invocation. Otherwise, the -type checker should generate an error. - -In cases similar to the ``bar`` function above the problem could be worked -around by explicitly dereferencing desired fields and using them as arguments -to perform the function call:: - - def bar(**kwargs: Unpack[Animal]): - name = kwargs["name"] - takes_name(name) +Functions typed with ``Unpack`` should be expanded to their explicit signatures +and assignment correctness should be determined by already existing rules. Using ``Unpack`` with types other than ``TypedDict`` ---------------------------------------------------- From daec49b21ba178201447322ec8267ce9df792e91 Mon Sep 17 00:00:00 2001 From: Franek Magiera Date: Mon, 28 Aug 2023 08:39:10 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Jelle Zijlstra --- pep-0692.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pep-0692.rst b/pep-0692.rst index eecb9c3b997..d1906085f38 100644 --- a/pep-0692.rst +++ b/pep-0692.rst @@ -126,7 +126,7 @@ type ``int``). This indicates that the function should be called as follows:: For the purposes of type checking, functions annotaded with ``Unpack`` should be considered equivalent to their explicitly expanded counterparts. Therefore, -from the type checkers point of view:: +from the type checker's point of view:: foo(**kwargs: Unpack[Movie]) -> None: ...