Skip to content

TypedDict in dynamic contexts #13940

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

Closed
jzazo opened this issue Oct 24, 2022 · 3 comments
Closed

TypedDict in dynamic contexts #13940

jzazo opened this issue Oct 24, 2022 · 3 comments
Labels

Comments

@jzazo
Copy link

jzazo commented Oct 24, 2022

Feature

I would like to be able to instantiate TypedDicts in dynamic contexts, for example:

tdict: dict[str, type] = {}
tdict["foo"] = int
tdict["bar"] = bool
booOutput = TypedDict("booOutput", tdict)

def boo(**kwargs) -> booOutput:
    return booOutput(**kwargs)

boo(foo=2, bar=False)

Pitch

I want to declare dynamic classes where I don't know statically what a method will return. But when I build the class I know the specific output types, and I would like to declare them so that I can rely on inspect.get_annotations or equivalent.

If TypedDict is not an option, is there a way that I can specify a return annotation dynamically when I create the class (a mapping return annotation)? This issue might be related. Thanks!

@jzazo jzazo added the feature label Oct 24, 2022
@jzazo jzazo changed the title TypedDict's in dynamic contexts TypedDict in dynamic contexts Oct 24, 2022
@erictraut
Copy link

erictraut commented Oct 24, 2022

This is also related to a recent discussion in the typing-sig. The consensus in that discussion was that TypedDict was designed for static type checking and that it is reasonable for static type checkers to emit errors if a TypedDict is constructed using dynamic arguments.

@hauntsaninja
Copy link
Collaborator

Not mentioned in the typing-sig thread, but NewType can also work for some of these use cases.

@hauntsaninja hauntsaninja closed this as not planned Won't fix, can't repro, duplicate, stale Oct 24, 2022
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.
@felixvd
Copy link

felixvd commented May 1, 2023

This is also related to a recent discussion in the typing-sig.

[from the discussion]
I think I'd want to see some additional signal that your use case is something that is more broadly desirable before adding this support to pyright.

-Eric

Not sure if this is the right place, but I found this discussion trying to implement the use case @pbryan described in that thread: validating code with types derived from a schema. Packages like jsonschema-typed and jsonschema-gentypes also target this use case, and the commit above that is referencing this thread from a repo with >100 stars also says It would be convenient to generate types dynamically [in this manner], so I believe it is somewhat desirable.

I have not found a good workflow or workaround for this yet, but I agree that it would be very helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants