I was wandering on the internets, browsing Analogue Pocket reviews before buying it for myself. Then I decided to do something a little bit less productive: finish and publish this article. Part one, part two.


Discovering and playing with Python type annotations is really fun and interesting. Especially for someone who never really used them before like me.

Here is a perfectly harmeless Python function that return something depending on something:

def foo(x):
    if x == 1:
        return 42
    elif x == 2:
        return "bar"
        raise TypeError("Invalid argument")

If we’re asked to add type annotations to this function, we can do the following:

from typing import Union

def foo(x: int) -> Union[int, str]:
    if x == 1:
        return 42
    elif x == 2:
        return "bar"
        raise TypeError("Invalid argument")

We’re comparing x variable to the integers 1 & 2, so it sounds like our function will take an Integer as parameter. Depending on some conditions, we’re returning an Integer or a String. So, foo is a function that takes an Integer and returns either an Integer or a String. Mypy seems to be happy:

> mypy
Success: no issues found in 1 source file

What’s enthralling:

It is interesting to see that, from Mypy perspective, the revealed types for the variables a and b are exactly the same:

a = foo(1)  # We know a is an Integer
b = foo(2)  # We know b is a String

# Yet
> mypy  # Yet note: Revealed local types are: note:     a: Union[, builtins.str] note:     b: Union[, builtins.str]

Even more enthralling:

c: int
c = foo(1) # Forbidden, even if the returned value is indeed an Integer

foo(2) + "bar" # Forbidden, even if the returned value is indeed a String
> mypy  # Yet again error: Incompatible types in assignment (expression has type "Union[int, str]", variable has type "int") error: Unsupported operand types for + ("int" and "str") note: Left operand is of type "Union[int, str]"
Found 2 errors in 1 file (checked 1 source file)

Relationship, argument, return types:

There is a way to tell Mypy that there is a relationship between the argument and the type of the returned value. There is way to annotate the foo function so that reveal_locals will be able to more precisely tells us what are the types for a & b. Overloads to the rescue.

An overloaded function must consist of two or more overload variants followed by an implementation:

from typing import overload, Literal

def foo(x: Literal[1]) -> int: ...

def foo(x: Literal[2]) -> str: ...

def foo(x):
    if x == 1:
        return 42
    elif x == 2:
        return "bar"
        raise TypeError("invalid argument")
a = foo(1)
b = foo(2)
>> mypy note: Revealed local types are: note:     a: note:     b: builtins.str
e: int
e = foo(1)

f: str
f = foo(2)

foo(1) + 1
foo(2) + "bar"
>> mypy
Success: no issues found in 1 source file

Can we have some real use cases?

We’ve seen a very simple use for variants, the following are some uses of them inside the stdlib.

The pow function:

From the documentation, we can read:

Help on built-in function pow in module builtins:

pow(base, exp, mod=None)
    Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments
    Some types, such as ints, are able to use a more efficient algorithm when
    invoked using the three argument form.

Let’s play a little bit with this function:

  1. if the 3rd argument equals zero, don’t return:

    In [4]: pow(foo, 2, 0)  
    ValueError                                Traceback (most recent call last)
    <ipython-input-4-9eeac4b049b8> in <module>
    ----> 1 pow(foo, 2, 0)
    ValueError: pow() 3rd argument cannot be 0
  2. if the 2nd argument is a positive Integer, return an Integer:

    In [5]: pow(foo, 2, 3)  
    Out[5]: 1
  3. if the second argument is a negative Integer, return a Float:

    In [6]: pow(foo, -2)
    Out[6]: 0.0625
    In [7]: 

The truth is, all the above witnessed behaviour are correctly documented here:

    def __pow__(self, __x: int, __modulo: Literal[0]) -> NoReturn: ...
    def __pow__(self, __x: int, __modulo: int) -> int: ...
    def __pow__(self, __x: _PositiveInteger, __modulo: None = ...) -> int: ...
    def __pow__(self, __x: _NegativeInteger, __modulo: None = ...) -> float: ...
    # positive x -> int; negative x -> float
    # return type must be Any as `int | float` causes too many false-positive errors
    def __pow__(self, __x: int, __modulo: None = ...) -> Any: ...

The gcd function:

Before Python 3.9, the gcd function was defined inside the fractions module. The function was receiving 2 arguments. If one of those arguments was an Integral, the returned value was an Integral:

Python 2.7.18 (default, Sep 10 2021, 14:59:31) 
[GCC 11.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import fractions
>>> fractions.gcd(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: gcd() takes exactly 2 arguments (3 given)
>>> fractions.gcd(1, 2)

The related type annotations:

if sys.version_info < (3, 9):
    def gcd(a: int, b: int) -> int: ...
    def gcd(a: Integral, b: int) -> Integral: ...
    def gcd(a: int, b: Integral) -> Integral: ...
    def gcd(a: Integral, b: Integral) -> Integral: ...

Since 3.9, it is defined inside the math module. Now, gcd takes only one argument, a sequence of Integers, and returns an Integer:

if sys.version_info >= (3, 9):
    def gcd(*integers: SupportsIndex) -> int: ...

    def gcd(__x: SupportsIndex, __y: SupportsIndex) -> int: ...
mind blown

mind blown

Have you ever used overloads?

I’ve used them once, I was contributing some type annotations to the Typeshed repository, for the Dateparser package. Never used them during the last couple of months I spent adding type annotations to RQL.

Overload of informations on this topic:

Really hope you’ve learned somthing. In another article, I’ll try to show you a spellbinding use of overloads + decorators. Before that, let me suggest the following links for an in depth exploration:

  1. Function method overloading
  2. AnyOf - Union for return types
  3. Document why having a union return type is often a problem

overload in progress

overload in progress