Precise typing of kwargs but without a TypedDict

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.

Some related threads:

3 Likes