Skip to content
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

TypedDict keys reuse? #4128

Open
jstasiak opened this issue Oct 17, 2017 · 8 comments
Open

TypedDict keys reuse? #4128

jstasiak opened this issue Oct 17, 2017 · 8 comments

Comments

@jstasiak
Copy link
Contributor

jstasiak commented Oct 17, 2017

I may be in a need of the following - two TypeDicts with the same keys, one of them created with total=False:

from mypy_extensions import TypedDict

Details = TypedDict('Details', {'name': str, 'age': int, 'address': str})
DetailsSubset = TypedDict('DetailsSubset', {'name': str, 'age': int, 'address': str}, total=False)

Is there a way to reuse the keys instead of having to repeat the {'name': str, 'age': int, address: str} fragment twice?

I can't do

from mypy_extensions import TypedDict

keys = {'name': str, 'age': int, 'address': str}
Details = TypedDict('Details', keys)
DetailsSubset = TypedDict('DetailsSubset', keys, total=False)

Because

% mypy code.py
code.py:4: error: TypedDict() expects a dictionary literal as the second argument
code.py:5: error: TypedDict() expects a dictionary literal as the second argument

(which is quite reasonable, tried that on the off chance it'd work)

@ilevkivskyi
Copy link
Member

This is currently not possible. We will probably need the Const type modifier for this to work, see #1214

@JukkaL
Copy link
Collaborator

JukkaL commented Nov 16, 2018

We now have a Final modified but that isn't enough, since we never implicitly infer TypedDict types from dict expressions.

@ilevkivskyi
Copy link
Member

Another4 request for this appeared, see #8186, so raising priority to normal.

@ilevkivskyi
Copy link
Member

ilevkivskyi commented Dec 20, 2019

(Just to clarify, we can start from only supporting final names for individual keys, which should be pretty easy.)

@cwhy
Copy link

cwhy commented Mar 12, 2020

@ilevkivskyi I agree that we can just support final names for keys first, at least for NamedTuple, as stated at the end of PEP-591:

Type checkers should treat uses of a final name that was initialized with a literal as if it was replaced by the literal. For example, the following should be allowed:


X: Final = "x"
Y: Final = "y"
N = NamedTuple("N", [(X, int), (Y, int)])

It is sometimes useful to have different types on the same label as well:

X: Final = "x"
Y: Final = "y"
Position = NamedTuple("Position", [(X, int), (Y, int)])
DimensionName = NamedTuple("DimensionName", [(X, str), (Y, str)])

@tommyjcarpenter
Copy link

tommyjcarpenter commented Mar 7, 2022

is there any update on this issue when Final is used? This seems last updated about two years ago

I would expect this to be legal:

In [3]: from typing import TypedDict, List, Final

In [6]: X: Final = "namespaces_read"
In [7]: Y: Final = "namespaces_write"

In [4]: Permissions = TypedDict(
   ...:     "Permissions", {X: List[str], Y: List[str]}
   ...: )

In [8]: Z: Permissions = {X: [], Y: []}

@tony
Copy link

tony commented Jul 27, 2022

Let me know if you'd like me to remove this and create a separate issue if it's off scope. This falls under the canopy of TypedDict keys reuse, but it looks like this issue came to be about subsets, when I want reuse across keyed structures

Allow all "typed fields" / keyed data structures to reuse their typings.

e.g. typing.TypedDict, typing.NamedTuple and dataclasses.dataclass.

Scenario: Typed exports from dataclass

Assume my case, I want to offer users ability to export a typed tuple and dict from my Details dataclass, dataclasses.astuple and dataclasses.asdict

#!/usr/bin/env python
import dataclasses
from typing import NamedTuple, TypedDict, get_type_hints


class DetailsDict(TypedDict):
    name: str
    age: int
    address: str


class DetailsTuple(NamedTuple):
    name: str
    age: int
    address: str


@dataclasses.dataclass
class Details:
    name: str
    age: int
    address: str

    def to_dict(self) -> DetailsDict:
        # return dataclasses.asdict(self, dict_factory=DetailsDict)
        return DetailsDict(**dataclasses.asdict(self))

    def to_tuple(self) -> DetailsTuple:
        # return dataclasses.astuple(self, tuple_factory=DetailsTuple)
        return DetailsTuple(*dataclasses.astuple(self))


john = Details(name="John", age=25, address="123 Address St")
print(john)
print(john.to_dict())
print(john.to_tuple())
print(get_type_hints(john))
print(get_type_hints(john.to_dict))
print(get_type_hints(john.to_tuple))

Output:

Details(name='John', age=25, address='123 Address St')
{'name': 'John', 'age': 25, 'address': '123 Address St'}
DetailsTuple(name='John', age=25, address='123 Address St')
{'name': <class 'str'>, 'age': <class 'int'>, 'address': <class 'str'>}
{'return': <class '__main__.DetailsDict'>}
{'return': <class '__main__.DetailsTuple'>

Why would a user want to reuse dict/tuple/etc?

  • To maintain mypy strict = True compliance
    • inside of unit testing / assertions. In my case, I have typed pytest fixtures
  • They have an open source library and want tuples / dicts to be available for downstream use.
    • Spreading into functions *params, **params, e.g. loading data

    • Reuse in args/kwargs of function declarations, e.g.

      Details(*DetailsTuple(name='John', age=25, address='123 Address St'))
      Details(**DetailsDict(**{'name': 'John', 'age': 25, 'address': '123 Address St'}))

Problem

We have to redeclare the same field annotations for dict, tuple, and dataclass

Solution

We need a way to reuse the field typings across the different keyed data structures.

@nitedani
Copy link

nitedani commented Sep 30, 2022

Why would a user want to reuse dict/tuple/etc?

class CreateArgs(TypedDict):
    first_name: str
    last_name: str
    external_id: str
    address: str
    city: str
    phone: str
    zip: str
    email: str
    state: str


class UpdateArgs(TypedDict, total=False):
    first_name: str
    last_name: str
    external_id: str
    address: str
    city: str
    phone: str
    zip: str
    email: str
    state: str
type CreateArgs = {
  first_name: string;
  last_name: string;
  external_id: string;
  address: string;
  city: string;
  phone: string;
  zip: string;
  email: string;
  state: string;
};

type UpdateArgs = Partial<CreateArgs>;

For me it's not about compliance, neither an open-source library.
It's code repetition.
I would like to define the keys only once.

br3ndonland added a commit to br3ndonland/inboard that referenced this issue Dec 6, 2022
This commit will enable mypy strict mode, and update code accordingly.

Type annotations are not used at runtime. The standard library `typing`
module includes a `TYPE_CHECKING` constant that is `False` at runtime,
but `True` when conducting static type checking prior to runtime. Type
imports will be included under `if TYPE_CHECKING:` conditions. These
conditions will be ignored when calculating test coverage.
https://docs.python.org/3/library/typing.html

The Python standard library `logging.config` module uses type stubs.
The typeshed types for the `logging.config` module are used solely for
type-checking usage of the `logging.config` module itself. They cannot
be imported and used to type annotate other modules. For this reason,
dict config types will be vendored into a module in the inboard package.
https://github.com/python/typeshed/blob/main/stdlib/logging/config.pyi

The ASGI application in `inboard.app.main_base` will be updated to ASGI3
and type-annotated with `asgiref.typing`. Note that, while Uvicorn uses
`asgiref.typing`, Starlette does not. The type signature expected by the
Starlette/FastAPI `TestClient` therefore does not match
`asgiref.typing.ASGIApplication`. A mypy `type: ignore[arg-type]`
comment will be used to resolve this difference.
https://asgi.readthedocs.io/en/stable/specs/main.html

Also note that, while the `asgiref` package was a runtime dependency of
Uvicorn 0.17.6, it was later removed from Uvicorn's runtime dependencies
in 0.18.0 (encode/uvicorn#1305, encode/uvicorn#1532). However, `asgiref`
is still used to type-annotate Uvicorn, so any downstream projects like
inboard that type-check Uvicorn objects must also install `asgiref`.
Therefore, `asgiref` will be added to inboard's development dependencies
to ensure that type checking continues to work as expected.

A Uvicorn options type will be added to a new inboard types module.
The Uvicorn options type will be a `TypedDict` with fields corresponding
to arguments to `uvicorn.run`. This type can be used to check arguments
passed to `uvicorn.run`, which is how `inboard.start` runs Uvicorn.

Uvicorn 0.17.6 is not fully type-annotated, and Uvicorn does not ship
with a `py.typed` marker file until 0.19.0.

It would be convenient to generate types dynamically with something like
`getattr(uvicorn.run, "__annotations__")` (Python 3.9 or earlier)
or `inspect.get_annotations(uvicorn.run)` (Python 3.10 or later).
https://docs.python.org/3/howto/annotations.html

It could look something like this:

```py
UvicornOptions = TypedDict(  # type: ignore[misc]
    "UvicornOptions",
    inspect.get_annotations(uvicorn.run),
    total=False,
)
```

Note the `type: ignore[misc]` comment. Mypy raises a `misc` error:
`TypedDict() expects a dictionary literal as the second argument`.
Unfortunately, `TypedDict` types are not intended to be generated
dynamically, because they exist for the benefit of static type checking
(python/mypy#3932, python/mypy#4128, python/mypy#13940).

Furthermore, prior to Uvicorn 0.18.0, `uvicorn.run()` didn't enumerate
keyword arguments, but instead accepted `kwargs` and passed them to
`uvicorn.Config.__init__()` (encode/uvicorn#1423). The annotations from
`uvicorn.Config.__init__()` would need to be used instead. Even after
Uvicorn 0.18.0, the signatures of the two functions are not exactly the
same (encode/uvicorn#1545), so it helps to have a static type defined.

There will be some other differences from `uvicorn.run()`:

- The `app` argument to `uvicorn.run()` accepts an un-parametrized
  `Callable` because Uvicorn tests use callables (encode/uvicorn#1067).
  It is not necessary for other packages to accept `Callable`, and it
  would need to be parametrized to pass mypy strict mode anyway.
  For these reasons, `Callable` will not be accepted in this type.
- The `log_config` argument will use the new inboard dict config type
  instead of `dict[str, Any]` for stricter type checking.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants