Can typing.overload be refined to explicitly allow "fallback" to the non-overloaded return type?

Here’s a common pattern I find myself writing in different contexts:

def get_snork_from_config() -> str | None:
    ...

Later, it turns out that the handling for snork being null/missing is common, so I add this:

def get_snork_from_config(allow_null: bool = False) -> str | None:
    ...

However, this typically doesn’t scan cleanly to type checkers, so it becomes an overloaded definition. What is, IMO, the “natural” way to write this definition is currently treated by mypy as an error (invalid overloads). I would like to write this:

@overload
def get_snork_from_config(allow_null: Literal[True]) -> str | None:
    ...

def get_snork_from_config(allow_null: bool = False) -> str:
    ...

But for this to work, I must have an overload which exactly matches the implementation’s parameter definition, and declares the return type:

@overload
def get_snork_from_config(allow_null: Literal[True]) -> str | None:
    ...

@overload
def get_snork_from_config(allow_null: bool = False) -> str:
    ...

# but I still need to annotate the runtime definition if I'm checking with
# mypy --disallow-untyped-defs / --strict
def get_snork_from_config(allow_null: bool = False) -> str | None:
    ...

As far as I can tell, the typing docs and PEP 484 do not stipulate that multiple overloads must be applied.

With a more complex method with many parameters, these overloads become quite awkward to read and write, as the full suite of parameters need to match the runtime definition.

Trying to think about and understand why multiple overloads are expected, the best I can guess is that there may be a concern about accidental failover behavior.
Therefore, would the following be an acceptable solution, although it requires a new symbol in typing, etc etc?

@overload
def get_snork_from_config(allow_null: Literal[True]) -> str | None:
    ...

@overload_default  # no-op decorator
def get_snork_from_config(allow_null: bool = False) -> str:
    ...

(I recognize that overload_default is not quite a no-op, as it needs to touch the runtime-visible overloads registry.)

I ask/suggest this because I find the requisite “correct” form sufficiently verbose and distracting that for even moderately complex APIs, a secondary method is simpler. e.g.

def get_snork_from_config() -> str | None:
    ...

def get_and_check_snork_from_config() -> str:
    ...

This requires that I write the types for all of my parameters twice, rather than three times.

2 Likes