PEP-0692 introduced precise type hinting of kwargs with a combination of Unpack and a TypedDict.
Issues:
-
Verbose. You need to actually write a TypedDict that matches your function’s kwargs and also correctly type hint each of them. Some libraries might not even expose all of their types to their public API so you’ll have to use the private types which might change without notice.
-
Duping. You’re duping code that already exists.
-
Error prone. We are humans and we make mistakes. It’s quite easy to miss or wrongly type hint an argument in your TypedDict. This only gets worse the more complicated your function is. No type checker will warn you about this.
-
Not so precise. Even if you ignore all of the above, you simply cannot match the TypedDict 1:1 with the function signature due to the lack of support for right hand side values (used to indicate the default)
-
Maintenance burden. This TypedDict needs to be constantly kept in sync manually in case your function (which might be from a third party library you have no control over) gains or loses any kwargs
Here’s a real world example of me trying to type hint my class that exposes httpx.Client()
kwargs to the end user that should show all the issues I’ve pointed above.
httpx.Client()
def __init__(
self,
*,
auth: AuthTypes | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
verify: VerifyTypes = True,
cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
proxy: ProxyTypes | None = None,
proxies: ProxiesTypes | None = None,
mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URLTypes = "",
transport: BaseTransport | None = None,
app: typing.Callable[..., typing.Any] | None = None,
trust_env: bool = True,
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
```py
TypedDict
from typing import TYPE_CHECKING, Any, Callable, Mapping, TypedDict
if TYPE_CHECKING:
from httpx import BaseTransport, Limits
# httpx doesn't expose the rest of types
# so I had to go find these undocumented types
from httpx._client import EventHook
from httpx._types import (
AuthTypes,
CertTypes,
CookieTypes,
HeaderTypes,
ProxiesTypes,
ProxyTypes,
QueryParamTypes,
TimeoutTypes,
URLTypes,
VerifyTypes,
)
# Note, I can't even match it 1:1 because TypedDicts don't support
# right hand-side values used to indicate the default
class HTTPXClientKwargs(TypedDict):
auth: AuthTypes | None
params: QueryParamTypes | None
headers: HeaderTypes | None
cookies: CookieTypes | None
verify: VerifyTypes
cert: CertTypes | None
http1: bool
http2: bool
proxy: ProxyTypes | None
proxies: ProxiesTypes | None
mounts: None | (Mapping[str, BaseTransport | None])
timeout: TimeoutTypes
follow_redirects: bool
limits: Limits
max_redirects: int
event_hooks: None | (Mapping[str, list[EventHook]])
base_url: URLTypes
transport: BaseTransport | None
app: Callable[..., Any] | None
trust_env: bool
default_encoding: str | Callable[[bytes], str]
Now that I’ve hopefully conveyed my issues with TypedDict, here’s what’s in my head:
Allow Unpack to accept arbitrary function and classes and automatically use their signature to type hint kwargs.
The above TypedDict is then no longer required and the code can be reduced down to:
from typing import Unpack
from httpx import Client
def my_wrapper(foo: str, **kwargs: Unpack[Client]) -> str:
with Client(**kwargs) as client:
response = client.get(foo, ...)
return response.text
Something like this would address all the issues I’ve pointed out above.
Hopefully I’ve conveyed my idea well. I’m new to Python and very interested in typing and have been using it from day 1. I hope none of the above comes off as rude or condescending in anyway, that’s not my intention. Thank you.