-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
PEP xxx: Inner Fields Annotations #3326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,322 @@ | ||||||||||||
PEP: 728 | ||||||||||||
Title: Inner Fields Annotations | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you be a bit more specific with this title? Before reading the PEP I had no idea what "Inner Fields Annotations" meant. |
||||||||||||
Author: Nir Schulman <[email protected]> | ||||||||||||
Sponsor: Jelle Zijlstra <[email protected]> | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ping @JelleZijlstra for confirmation of sponsorship There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, but I am not sponsoring this PEP at the moment. I already have four open PEPs (696, 702, 724, 727), that feels like too much. I can think about sponsoring more when a few of the others are done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. In that case I'll wait and in the meantime address the rest of the comments (and in general try to improve the idea). @AA-Turner Does this mean this PR should be closed for now or should it stay open with an appropriate label? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @UltimateLobster I would suggest that you revisit the Discourse thread and see if another core developer interested in typing would sponsor this proposal. I'll close this PR for now so that we're aware that the PEP number isn't reserved, though still happy to discuss any of the comments I made in review. A There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to set expectations, I don't know whether I'd be willing to sponsor the PEP if I had fewer open PEPs; I'd have to think more about whether I think this is a good direction to go into. I'd also prefer to see more evidence of the feasibility of the idea (e.g., buy-in from type checker maintainers, ideally a prototype implementation). |
||||||||||||
Discussions-To: https://discuss.python.org/t/26527 | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This link should go into Post-History; a new thread should be created for PEP 728 just before this PR is merged |
||||||||||||
Status: Draft | ||||||||||||
Type: Standards Track | ||||||||||||
Topic: Typing | ||||||||||||
Content-Type: text/x-rst | ||||||||||||
Created: 01-Sep-2023 | ||||||||||||
Python-Version: 3.13 | ||||||||||||
Post-History: | ||||||||||||
|
||||||||||||
|
||||||||||||
Abstract | ||||||||||||
======== | ||||||||||||
|
||||||||||||
This PEP specifies a way for annotations to reference the field types of other complex objects. This include the field | ||||||||||||
names and types objects with static layout such as dataclasses (or dataclass-like objects), TypedDicts and NamedTuples. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
|
||||||||||||
Motivation | ||||||||||||
========== | ||||||||||||
Consider the following example: | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Cat: | ||||||||||||
fur_color: str | ||||||||||||
age: int | ||||||||||||
|
||||||||||||
def get_cats(field_name: Literal["fur_color", "age"], value: str | int) -> list[Cat]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
# will return cats that are 3 years old | ||||||||||||
get_cats("age", 3) | ||||||||||||
|
||||||||||||
|
||||||||||||
The current annotation of ``get_cats`` has some problems: | ||||||||||||
|
||||||||||||
1. We need to remember to update this function whenever the field types or names of ``Cat`` are changing. | ||||||||||||
2. We lose the connection between the field name and the field type. ``get_cats("age", "black")`` is invalid usage would | ||||||||||||
because we supplied a non-integer value as the filter for the age field, but the type checker allows this. | ||||||||||||
|
||||||||||||
The latter problem can be solved using ``typing.overload``: | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import overload, Literal | ||||||||||||
|
||||||||||||
@overload | ||||||||||||
def get_cats(field_name: Literal['fur_color'], value: str) -> list[Cat]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
@overload | ||||||||||||
def get_cats(field_name: Literal['age'], value: int) -> list[Cat]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
def get_cats(field_name, value) -> list[Cat]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
However this solution only worsens the former problem. We need to recreate the definition of the ``get_cats`` function | ||||||||||||
for each field, and remember to update the relevant overload whenever the class ``Cat`` changes. This is cumbersome and | ||||||||||||
introduces a lot of code duplication (which is prone to errors). | ||||||||||||
|
||||||||||||
The current proposals solves both of the mentioned problems by introducing 2 new types into the ``typing`` module. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
``Field`` and ``FieldName`` which allows you to refer to the type of an object's fields as well as their respective names. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
|
||||||||||||
from typing import FieldName | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Cat: | ||||||||||||
fur_color: str | ||||||||||||
age: int | ||||||||||||
|
||||||||||||
def get_cats[T: FieldName[Cat]](field_name: T, value: Field[Cat, T]) -> list[Cat]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
get_cats("some_field", 12) # Type checker error: Cat did not define field named "some_field" | ||||||||||||
get_cats("age", "black") # Type checker error: Cat defines "age" as a field of type int, but received str. | ||||||||||||
get_cats("age", 3) # Passes type check | ||||||||||||
|
||||||||||||
A similar benefit could be achieved for other functions that expose certain CRUD operations against a database. | ||||||||||||
Clients like pymongo can then define stricter typings for their queries: | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
|
||||||||||||
# This definition is obviously simplified for the sake of the example. | ||||||||||||
type Filter[T: dict, TName: FieldName[T]] = dict[TName, NotRequired[Field[T, TName]]] | ||||||||||||
|
||||||||||||
class Collection[T]: | ||||||||||||
def find(self, filter: Filter[T]) -> Iterable[T]: | ||||||||||||
... | ||||||||||||
|
||||||||||||
cat_collection: Collection[Cat] | ||||||||||||
|
||||||||||||
# Passes type checking because filters do not require all of Cat's fields to be present within the query. | ||||||||||||
cat_collection.find({"age": 3}) | ||||||||||||
|
||||||||||||
# Fails type checking because fur_color must be a string | ||||||||||||
cat_collection.find({"fur_color": 1}) | ||||||||||||
|
||||||||||||
# Fails type checking because "foo" is not declared in the collection's scheme | ||||||||||||
cat_collection.find({"foo": "bar"}) | ||||||||||||
|
||||||||||||
|
||||||||||||
Specification | ||||||||||||
============= | ||||||||||||
|
||||||||||||
The current proposals introduces adds ``Field`` and ``FieldName`` to the ``typing`` module. | ||||||||||||
|
||||||||||||
Field | ||||||||||||
''''''''' | ||||||||||||
Comment on lines
+115
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
``Field`` can be used to refer to another type's fields. It receives 2 arguments, a type and a name of a field within | ||||||||||||
this type. The first argument may be a dataclasses, (or dataclass-transformed objects), classes implementing | ||||||||||||
``__slots__``, ``TypedDict`` or any other type with a typed layout. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Function returns int | ||||||||||||
def func() -> Field[Foo, 'bar']: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
While dataclass-like objects will consider the attributes of the instance, dictionaries will instead refer to the items | ||||||||||||
of the instance. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field, TypedDict | ||||||||||||
|
||||||||||||
class Foo(TypedDict): | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Function returns int | ||||||||||||
def func() -> Field[Foo, 'bar']: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
|
||||||||||||
Multiple field names may be specified using ``Literal``. In these cases, the annotation will be the | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
equivalent of a union of the types of the given fields. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field, Literal | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Function returns int | str | ||||||||||||
def func() -> Field[Foo, Literal['bar', 'baz']]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
Using ``Field`` with an explicit ``Literal`` of a single field is allowed, but not needed because you can use the bare | ||||||||||||
field name. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field, Literal | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Function returns int | ||||||||||||
def func() -> Field[Foo, Literal["bar"]]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
|
||||||||||||
String field names that are provided to ``Field`` will always be assumed to be the literal strings (instead of forward | ||||||||||||
references) unless the entire annotation is wrapped within quotes. When the entire annotation is wrapped within quotes, | ||||||||||||
you may still explicitly use ``Literal`` in order to prevent this behavior. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field, Literal | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
bar: TypeAlias = Literal['baz'] | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this use the type parameter syntax since TypeAlias is deprecated(-ish)? They technically don't do the same thing at runtime but I'm guessing both are supposed to work the same within the context of this PEP's new typing constructs. |
||||||||||||
|
||||||||||||
# Will refer to the literal "bar". Function returns int | ||||||||||||
def func() -> Field[Foo, 'bar']: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
# Will refer to the type alias bar. Function returns str | ||||||||||||
def func() -> 'Field[Foo, bar]': | ||||||||||||
pass | ||||||||||||
|
||||||||||||
# Will refer to a union of the literal "bar" and the type alias bar. Function returns int | str | ||||||||||||
def func() -> 'Field[Foo, Literal[bar] | bar]': | ||||||||||||
pass | ||||||||||||
|
||||||||||||
If no field names are given to ``Field``, the annotation will be an equivalent of a union of all values of the fields | ||||||||||||
within the type | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Function returns int | str | ||||||||||||
def func() -> Field[Foo]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
Type checkers should raise an error when using an invalid field name as a parameter to ``Field`` | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import Field | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# Type checker error, "Foo" does not expose a field named "bla" | ||||||||||||
def func() -> Field[Foo, "bla"]: | ||||||||||||
pass | ||||||||||||
|
||||||||||||
|
||||||||||||
FieldName | ||||||||||||
''''''''' | ||||||||||||
Comment on lines
+242
to
+243
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
In addition to ``Field``, ``FieldName`` will be added to the ``typing`` module. ``FieldName`` can be used in order to | ||||||||||||
refer to the name of a type's fields. It receives a single argument and when used, is equivalent to a union | ||||||||||||
of literals of its field names. | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
from typing import FieldName | ||||||||||||
|
||||||||||||
@dataclass | ||||||||||||
class Foo: | ||||||||||||
bar: int | ||||||||||||
baz: str | ||||||||||||
|
||||||||||||
# FieldName[ComplexType] is equivalent to Literal["bar", "baz"] | ||||||||||||
def func(field_name: FieldName[Foo]): | ||||||||||||
pass | ||||||||||||
|
||||||||||||
func("bar") | ||||||||||||
func("baz") | ||||||||||||
|
||||||||||||
# Type checker error. | ||||||||||||
func("something else") | ||||||||||||
|
||||||||||||
|
||||||||||||
|
||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
Open Issues | ||||||||||||
=========== | ||||||||||||
|
||||||||||||
Having separate types for attributes, and items | ||||||||||||
''''''''''''''''''''''''''''''''''''''''''''''' | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
``FieldName`` and ``Field`` can refer to either objects attributes (``obj.x``) or items ``(obj['x'])`` depending on the | ||||||||||||
context, this behavior may cause some confusion. One could suggest that instead of ``FieldName`` and ``Field`` being | ||||||||||||
added to typing, we could add ``Attribute``, ``AttributeName``, ``Item``, ``ItemName``. | ||||||||||||
|
||||||||||||
In general this approach has the benefit of being consistent and predictable from the user side of things. | ||||||||||||
``Attribute[ObjType, 'x']`` would be treated the type of ``obj.x`` whereas ``Item[ObjType, 'x']`` would be treated as | ||||||||||||
the type of ``obj['x']``. These types would have the same meaning for all python objects (Whether working with TypedDicts or | ||||||||||||
dataclasses). It would also allow referring to fields of objects containing both attributes and items: | ||||||||||||
|
||||||||||||
.. code-block:: py | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
class Foo(TypedDict): | ||||||||||||
get: int | ||||||||||||
set: str | ||||||||||||
|
||||||||||||
# Refers to the type of Foo['get'] | ||||||||||||
Item[Foo, 'get] | ||||||||||||
|
||||||||||||
# Refers to the dictionary method 'dict.get' | ||||||||||||
Attribute[Foo, 'get'] | ||||||||||||
|
||||||||||||
However, this approach will clutter the typing namespace with more concepts that would have to be maintained. It is | ||||||||||||
debatable whether or not it would make this concept easier or harder to learn. While requiring to understand 4 | ||||||||||||
additional utilities instead of only 2, one could argue it makes more intuitive sense and is easier to remember than the | ||||||||||||
behaviour of TypedDicts, dataclasses, and dataclass-like types separately. | ||||||||||||
|
||||||||||||
|
||||||||||||
Should __annotations__ be used as the reference table for fields? | ||||||||||||
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' | ||||||||||||
Comment on lines
+301
to
+302
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
Related to the previous question, we could define ``Field`` and ``FieldName`` by the type's __annotations__ definition. | ||||||||||||
This approach will allow for type-checkers to have the same treatment for dataclasses and TypedDict, making it easier to | ||||||||||||
support this feature. | ||||||||||||
|
||||||||||||
However, this approach will not allow users to refer to dictionaries' methods, or dataclass-like | ||||||||||||
types' items (if they support ``__getitem__``). | ||||||||||||
It might also prevent us from expanding these types in the future to refer to inner types of typed collections (like | ||||||||||||
``dict[int, str]``, ``tuple[int, ...]``, etc.) or any other type that does not define ``__annotations__``. | ||||||||||||
|
||||||||||||
|
||||||||||||
Naming | ||||||||||||
'''''' | ||||||||||||
Comment on lines
+313
to
+314
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
The names ``Field`` and ``FieldName`` are temporary and would probably need to change as this PEP evolves and the rest | ||||||||||||
of the questions in this section are answered. | ||||||||||||
|
||||||||||||
|
||||||||||||
Copyright | ||||||||||||
========= | ||||||||||||
|
||||||||||||
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please wrap to 80 columns per PEP 1