Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Type Annotations: Mark item in tuple as optional

I have a function that returns an 2-tuple but optionally a 3-tuple. How do I describe this with type annotations?

For example:

from typing import Tuple

def example(i):
    # type: (int) -> Tuple[int, int, <what to put here?>]
    if i < 10:
       return (i, i+1, 1)
    else:
       return (i, i+1)

I can use Union like below, but it seems quite messy.

# type: (int) -> Union[Tuple[int, int], Tuple[int, int, int]]
like image 850
Dewald Abrie Avatar asked Sep 22 '17 01:09

Dewald Abrie


People also ask

What is optional in Python typing?

In line with other type-hinted languages, the preferred (and more concise) way to denote an optional argument in Python 3.10 and up, is now Type | None , e.g. str | None or list | None .

What is TypeVar?

In short, a TypeVar is a variable you can use in type signatures so you can refer to the same unspecified type more than once, while a NewType is used to tell the type checker that some values should be treated as their own type.

How do you type hints in Python?

Here's how you can add type hints to our function: Add a colon and a data type after each function parameter. Add an arrow ( -> ) and a data type after the function to specify the return data type.

Does Python 3.6 support type hints?

This module supports type hints as specified by PEP 484 and PEP 526. The most fundamental support consists of the types Any , Union , Tuple , Callable , TypeVar , and Generic . For full specification please see PEP 484. For a simplified introduction to type hints see PEP 483.


1 Answers

As pointed out in the comments, the union is probably the better approach. If the signature looks messy, you can use type aliases like so:

from typing import Tuple, Union

MyType = Union[Tuple[int, int], Tuple[int, int, int]]

def example1(i):
    # type: (int) -> MyType
    ...snip...

An alternative approach would be to use the "indefinite length" Tuple type. You would basically give up on encoding the exact length of the tuple but in exchange can normalize the return type and avoid that union. (If your code does rely on the length, this probably isn't the best approach).

def example2(i):
    # type: (int) -> Tuple[int, ...]
    ...snip...

However, a somewhat more drastic approach would be to consider restructuring your code to avoid this type of situation.

After all, if you're returning these two distinct types, the caller of your function is probably going to need to check the length anyways, right?

In that case, one idea is to give up on returning a tuple and instead return either a NamedTuple or a custom class, both of which have an optional field. You can then convert your "length" check into a "is this field set to None" check.

I suppose, in a sense, the NamedTuple approach also fulfills your original request in a way, so long as you don't mind converting your Tuples/the added overhead.

from typing import NamedTuple, Optional

MyType2 = NamedTuple('MyType2', (
    ('x', int),
    ('y', int),
    ('z', Optional[int]),
))

class MyType3(object):
    def __init__(self, x, y, z):
        # type: (int, int, Optional[int]) -> None
        self.x = x
        self.y = y
        self.z = z

(The "custom class" approach will likely be more elegant once PEP 557 is accepted and integrated into the language).

The other approach is to split your function up into two if you know ahead of time which kind of tuple you're expecting. Then, you can just call the function that appropriate type.

like image 111
Michael0x2a Avatar answered Sep 19 '22 04:09

Michael0x2a