N3buchadnezzar
N3buchadnezzar

Reputation: 471

How to use typing hints with an optional first parameter

Introduction

I have a function that is either called as fun(start, stop, divisors) or fun(stop, divisors).

  1. I want to call the parameters in this specific order.
  2. I want to implement it in a way that does not give any type-hinting errors.

If I ease on any of these two restrictions, the implementation is easy. For instance, easing the restriction on the order can be done like so:

def fun(stop: int, divisors: List[int], start: int=0) -> int:
    ...
    return 0

fun(5, [1, 2, 3])
fun(5, [1, 2, 3], start=2)

Or easing on the type hints is shown in the file below, but with a proper ordering of arguments.

Question

How should I write my code so that my function arguments are:

  1. statically typed;
  2. in the particular order: (start[optional], stop, divisors);
  3. gives 0 mypy errors?

Attempts

import typing
from typing import Union, Optional

Start = int
Stop = int
Divisor = int
Divisors = list[Divisor]


@typing.overload
def fun1(x: Start, y: Stop, divisors: Divisors) -> int:
    ...


@typing.overload
def fun1(y: Stop, divisors: Divisors) -> int:
    ...


def fun1(*args) -> int:
    if (arglen := len(args)) not in [2, 3]:
        raise TypeError("Function expected 2 or 3 arguments, got", arglen)
    if arglen == 2:
        args = [0] + list(args)
    start, stop, divisors = args
    return 0


def fun2(x, y, divisors) -> int:
    if divisors is None:
        start, stop, divisors = 0, x, y
    else:
        start, stop = x, y
    return 0


def fun3(*args) -> int:
    if (arglen := len(args)) == 3:
        start: Stop = args[0]
        stop: int = args[1]
        divisors: Divisors = args[2]
    elif arglen == 2:
        start: Stop = 0
        stop: int = args[0]
        divisors: Divisors = args[1]
    else:
        raise TypeError(
            f"Too {'few' if arglen == 0 else 'many'} values to unpack (2-3), got",
            arglen,
        )
    return 0


def fun4(*args) -> int:
    if (arglen := len(args)) == 3:
        pass
    elif arglen == 2:
        args = [0] + list(args)
    else:
        raise TypeError(
            f"Too {'few' if arglen == 0 else 'many'} values to unpack (2-3), got",
            arglen,
        )
    start: Stop = args[0]
    stop: int = args[1]
    divisors: Divisors = args[2]
    return 0

Output from mypy:

typing_test_PE_001.py:20: error: Overloaded function implementation does not accept all possible arguments of signature 1
typing_test_PE_001.py:20: error: Overloaded function implementation does not accept all possible arguments of signature 2
typing_test_PE_001.py:24: error: Incompatible types in assignment (expression has type "List[int]", variable has type "Tuple[Any, ...]")
typing_test_PE_001.py:43: error: Name "start" already defined on line 39
typing_test_PE_001.py:44: error: Name "stop" already defined on line 40
typing_test_PE_001.py:45: error: Name "divisors" already defined on line 41
typing_test_PE_001.py:58: error: Incompatible types in assignment (expression has type "List[int]", variable has type "Tuple[Any, ...]")
Found 7 errors in 1 file (checked 1 source file)

Upvotes: 1

Views: 780

Answers (1)

chepner
chepner

Reputation: 530823

Let's see how slice is hinted:

class slice(object):
    start: Any
    step: Any
    stop: Any
    @overload
    def __init__(self, stop: Any) -> None: ...
    @overload
    def __init__(self, start: Any, stop: Any, step: Any = ...) -> None: ...
    __hash__: None  # type: ignore
    def indices(self, len: SupportsIndex) -> Tuple[int, int, int]: ...

and range

class range(Sequence[int]):
    start: int
    stop: int
    step: int
    @overload
    def __init__(self, stop: SupportsIndex) -> None: ...
    @overload
    def __init__(self, start: SupportsIndex, stop: SupportsIndex, step: SupportsIndex = ...) -> None: ...
    [...]

Basically, you hint the two overloaded versions separately, one with the optional first parameter and one without. (So basically, your Attempt #2.)

from typing import overload, List

@overload
def fun(start: int, stop: int, divisors: List[int]):
    ...

@overload
def fun(stop: int, divisors: List[int]):
    ...

def fun(start, stop, divisors=None):
    if divisors is None:
        divisors = stop
        stop = start
        start = 0

    ...

fun(1, 2, [1,2,3])  # OK
fun(2, [1,2,3])  # OK

If you like, you can also make both variants accept positional arguments only:

# e.g.
def fun(start, stop, divisors=None, /):
    ...

Upvotes: 4

Related Questions