-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Add experimental support for PEP 764 inline TypedDicts #18889
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
base: master
Are you sure you want to change the base?
Changes from all commits
839afb5
e31a00c
84edf6d
caafa17
9ff1cf9
440ca82
e45ef52
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 |
---|---|---|
|
@@ -294,35 +294,45 @@ Inline TypedDict types | |
|
||
.. note:: | ||
|
||
This is an experimental (non-standard) feature. Use | ||
``--enable-incomplete-feature=InlineTypedDict`` to enable. | ||
This is an experimental feature proposed by :pep:`764`. Use | ||
``--enable-incomplete-feature=NewInlineTypedDict`` to enable. | ||
|
||
Sometimes you may want to define a complex nested JSON schema, or annotate | ||
a one-off function that returns a TypedDict. In such cases it may be convenient | ||
to use inline TypedDict syntax. For example: | ||
|
||
.. code-block:: python | ||
|
||
def test_values() -> {"int": int, "str": str}: | ||
def test_values() -> TypedDict[{"int": int, "str": str}]: | ||
return {"int": 42, "str": "test"} | ||
|
||
class Response(TypedDict): | ||
status: int | ||
msg: str | ||
# Using inline syntax here avoids defining two additional TypedDicts. | ||
content: {"items": list[{"key": str, "value": str}]} | ||
content: TypedDict[{"items": list[TypedDict[{"key": str, "value": str}]]}] | ||
|
||
Inline TypedDicts can also by used as targets of type aliases, but due to | ||
ambiguity with a regular variables it is only allowed for (newer) explicit | ||
type alias forms: | ||
.. note:: | ||
|
||
.. code-block:: python | ||
Mypy also supports a legacy syntax for inline TypedDicts that pre-dates :pep:`764`: | ||
|
||
.. code-block:: python | ||
|
||
def test_values() -> {"int": int, "str": str}: | ||
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'm just some random guy, but my 2¢ here is that it will probably confuse people that the strings here are also the names of the types, as that's sort of ambiguous to the reader — "do they have to be the names of the types in this syntax, is that the rule?", you can imagine the reader asking himself. The subsequent examples make the usage clear in any case, though. |
||
return {"int": 42, "str": "test"} | ||
|
||
This legacy syntax can be enabled using ``--enable-incomplete-feature=InlineTypedDict``. | ||
Due to ambiguity with a regular variables, the legacy syntax may only be used in | ||
type aliases when using (newer) explicit type alias forms: | ||
|
||
.. code-block:: python | ||
|
||
from typing import TypeAlias | ||
from typing import TypeAlias | ||
|
||
X = {"a": int, "b": int} # creates a variable with type dict[str, type[int]] | ||
Y: TypeAlias = {"a": int, "b": int} # creates a type alias | ||
type Z = {"a": int, "b": int} # same as above (Python 3.12+ only) | ||
X = {"a": int, "b": int} # creates a variable with type dict[str, type[int]] | ||
Y: TypeAlias = {"a": int, "b": int} # creates a type alias | ||
type Z = {"a": int, "b": int} # same as above (Python 3.12+ only) | ||
|
||
Also, due to incompatibility with runtime type-checking it is strongly recommended | ||
to *not* use inline syntax in union types. | ||
This restriction does not apply to the :pep:`764` syntax. | ||
Also, due to incompatibility with runtime type-checking, it is strongly recommended | ||
to *not* use legacy inline syntax in union types. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3659,7 +3659,7 @@ reveal_type(x) # N: # N: Revealed type is "TypedDict({'int': builtins.int, 'st | |
|
||
[case testTypedDictInlineNoEmpty] | ||
# flags: --enable-incomplete-feature=InlineTypedDict | ||
x: {} # E: Invalid type comment or annotation | ||
x: {} # E: Legacy inline TypedDict must have at least one item | ||
reveal_type(x) # N: Revealed type is "Any" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
@@ -3707,6 +3707,134 @@ reveal_type(x) # N: Revealed type is "TypedDict({'a': builtins.int, 'b': builti | |
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-full.pyi] | ||
|
||
[case testTypedDictInlinePEP764NotEnabled] | ||
from typing import TypedDict | ||
x: TypedDict[{"a": int}] # E: PEP 764 inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=NewInlineTypedDict | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764OldStyleAlias] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
X = TypedDict[{"int": int, "str": str}] | ||
x: X | ||
reveal_type(x) # N: Revealed type is "TypedDict({'int': builtins.int, 'str': builtins.str})" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764MidStyleAlias] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
from typing_extensions import TypeAlias | ||
X: TypeAlias = TypedDict[{"int": int, "str": str}] | ||
x: X | ||
reveal_type(x) # N: Revealed type is "TypedDict({'int': builtins.int, 'str': builtins.str})" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764OYesEmpty] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
x: TypedDict[{}] | ||
reveal_type(x) # N: Revealed type is "TypedDict({})" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
|
||
[case testTypedDictInlinePEP764NotRequired] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import NotRequired, TypedDict | ||
|
||
x: TypedDict[{"one": int, "other": NotRequired[int]}] | ||
x = {"one": 1} # OK | ||
y: TypedDict[{"one": int, "other": int}] | ||
y = {"one": 1} # E: Expected TypedDict keys ("one", "other") but found only key "one" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764ReadOnly] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import ReadOnly, TypedDict | ||
|
||
x: TypedDict[{"one": int, "other": ReadOnly[int]}] | ||
x["one"] = 1 # ok | ||
x["other"] = 1 # E: ReadOnly TypedDict key "other" TypedDict is mutated | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764NestedSchema] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
def nested() -> TypedDict[{"one": str, "other": TypedDict[{"a": int, "b": int}]}]: | ||
brianschubert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if bool(): | ||
return {"one": "yes", "other": {"a": 1, "b": 2}} # OK | ||
else: | ||
return {"one": "no", "other": {"a": 1, "b": "2"}} # E: Incompatible types (expression has type "str", TypedDict item "b" has type "int") | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764MergeAnother] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypeVar, TypedDict | ||
from typing_extensions import TypeAlias | ||
|
||
T = TypeVar("T") | ||
X: TypeAlias = TypedDict[{"item": T}] | ||
x: TypedDict[{"a": int, **X[str], "b": int}] # E: PEP 764 inline TypedDict does not support merge-in | ||
reveal_type(x) # N: Revealed type is "Any" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-full.pyi] | ||
|
||
[case testTypedDictInlinePEP764BadArg] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
x: TypedDict[int] # E: Argument to TypedDict[] must be a literal dictionary mapping item names to types | ||
reveal_type(x) # N: Revealed type is "Any" | ||
y: TypedDict[{1: str}] # E: Argument to TypedDict[] must be a literal dictionary mapping item names to types | ||
reveal_type(y) # N: Revealed type is "Any" | ||
z: TypedDict[{"a": 1}] # E: Invalid type: try using Literal[1] instead? | ||
reveal_type(z) # N: Revealed type is "TypedDict({'a': Any})" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764TooManyArgs] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
x: TypedDict[{"a": int}, "foo"] # E: TypedDict[] must have exactly one type argument | ||
reveal_type(x) # N: Revealed type is "Any" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764TypeVarValid] | ||
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. Maybe we can add a test with PEP 695 type aliases? 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. there's one in |
||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import Generic, TypeVar, TypedDict | ||
T = TypeVar("T") | ||
class X(Generic[T]): | ||
attr: TypedDict[{'name': T}] | ||
def f(arg: T) -> TypedDict[{'name': T}]: ... | ||
Y = TypedDict[{'name': T}] | ||
reveal_type(X[int]().attr['name']) # N: Revealed type is "builtins.int" | ||
reveal_type(f('a')['name']) # N: Revealed type is "builtins.str" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
[case testTypedDictInlinePEP764TypeVarInvalid] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypeVar, TypedDict | ||
T = TypeVar('T') | ||
def f(): | ||
X = TypedDict[{'name': T}] # TODO: emit error - this is invalid per PEP-764 | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-full.pyi] | ||
|
||
[case testTypedDictInlinePEP764UsesExtensionSyntax] | ||
# flags: --enable-incomplete-feature=NewInlineTypedDict | ||
from typing import TypedDict | ||
x: TypedDict[{'name': str, 'production': {'location': str}}] # E: Legacy inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=InlineTypedDict \ | ||
# N: Did you mean TypedDict[...]? | ||
reveal_type(x) # N: Revealed type is "TypedDict({'name': builtins.str, 'production': TypedDict({'location': builtins.str})})" | ||
[builtins fixtures/dict.pyi] | ||
[typing fixtures/typing-typeddict.pyi] | ||
|
||
# ReadOnly | ||
# See: https://peps.python.org/pep-0705 | ||
|
Uh oh!
There was an error while loading. Please reload this page.