Skip to content

← Interfaces with Protocols and ABCs step 12 of 26

Medium Framework

Type **kwargs with a TypedDict and Unpack

open_connection(host, **kwargs) takes its options as keyword arguments, and it has a bug. It reads kwargs.get("timeoutt", 5.0), with two t's, so it ignores the timeout the caller passes and always uses 5.0. mypy says nothing, because the starter annotates **kwargs: Any, and Any turns checking off.

Describe the keywords with a TypedDict

The options already have a TypedDict. ConnOpts requires an integer port, and its timeout and tls keys are NotRequired, so a caller may leave them out. Wrap it in Unpack to tell mypy that each keyword argument is one key of ConnOpts:

def open_connection(host: str, **kwargs: Unpack[ConnOpts]) -> Connection: ...

Now mypy checks every call against ConnOpts. Leaving out port, passing a string for it, or writing timeoutt=2.5 is an error. Inside the function, kwargs has the type ConnOpts, so mypy also catches the misspelled key in the body. Change only the annotation, and mypy reports:

Argument "timeout" to "Connection" has incompatible type "object"; expected "float"

ConnOpts has no "timeoutt" key, so the best type mypy can give that .get() call is object.

What to write

  • Import Unpack from typing, and annotate **kwargs as Unpack[ConnOpts].
  • Fix the misspelled "timeoutt" key.
  • Use 5.0 when the caller leaves out timeout, and False when they leave out tls.
  • Keep a timeout of 0.0 when the caller passes one. A default applied with or would replace it.
  • Keep ConnOpts, Connection and solve as they are.

At runtime kwargs is still a plain dict. Unpack adds no defaults, so the defaults stay in the function body.

What the driver returns

solve(host, port, timeout) returns three strings from Connection.describe(), in this order:

  1. A connection with only port passed.
  2. A connection with port and timeout.
  3. A connection with port, timeout and tls=True.

Each string looks like tcp://db:5432?timeout=5.00, with tls in place of tcp when TLS is on. For ("db", 5432, 2.5), the three timeouts are 5.00, 2.50 and 2.50.

Store the function with its keywords

Callable[...] cannot describe keyword arguments, but a callback protocol can. With ConnOpts and Connection in scope, this type keeps every keyword's name and type:

from typing import Protocol, Unpack


class ConnectionFactory(Protocol):
    def __call__(self, host: str, **kwargs: Unpack[ConnOpts]) -> Connection: ...


factory: ConnectionFactory = open_connection

See Unpack for keyword arguments in the typing specification.