18
>>> from typing import overload

>>> @overload
... def hello(s: int):
...     return "Got an integer!"

>>> def hello(s: str):
...     return "Got a string"

Why does the calling hello(1) call the function with the string argument? Ideally, the @overload operator should handle it, right?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
saruftw
  • 1,104
  • 2
  • 14
  • 37
  • 1
    You need to decorate both definitions, otherwise you're just overriding whatever `hello` was with a normal function. – L3viathan Aug 27 '18 at 07:51
  • To be precise, only stub functions should be decorated. Calling decorated function will raise `NotImplementedError`. The idea of `overload` **is to overwrite stubs** with actual implementation. – awesoon Aug 27 '18 at 08:33

2 Answers2

29

Unfortunately, python does not allow function overloading. Each time you think you are overloading function, you are just overwriting previous function declaration. Quote from the docs:

The @overload decorator allows describing functions and methods that support multiple different combinations of argument types. A series of @overload-decorated definitions must be followed by exactly one non-@overload-decorated definition (for the same function/method). The @overload-decorated definitions are for the benefit of the type checker only, since they will be overwritten by the non-@overload-decorated definition, while the latter is used at runtime but should be ignored by a type checker. At runtime, calling a @overload-decorated function directly will raise NotImplementedError.

The correct usage of typing.overload is as follows:

from typing import overload


@overload
def hello(s: int) -> str:
    ...


@overload
def hello(s: str) -> str:
    ...


def hello(s):
    if isinstance(s, int):
        return "Got an integer!"
    if isinstance(s, str):
        return "Got a string"
    raise ValueError('You must pass either int or str')


if __name__ == '__main__':
    print(hello(1))

To show the actual benefit of typing.overload lets change def hello(s: int) to return int instead of str:

from typing import overload


@overload
def hello(s: int) -> int:
    ...


@overload
def hello(s: str) -> str:
    ...


def hello(s):
    if isinstance(s, int):
        return "Got an integer!"
    if isinstance(s, str):
        return "Got a string"
    raise ValueError('You must pass either int or str')


if __name__ == '__main__':
    print(hello(1))
    a = hello(1) + 1
    b = hello(1) + 'a'

Note, that the actual implementation still returns str - python does not perform any checks here. However, PyCharm raises a warning:

enter image description here

mypy also complains about invalid types:

➜ mypy test.py 
test.py:25: error: Unsupported operand types for + ("int" and "str")

The purpose of typing module is to allow third party tools to perform static checking of your code. There is no magic here - all types are ignored at runtime.

awesoon
  • 32,469
  • 11
  • 74
  • 99
  • Then what's the point of `@overload` if we're anyway going to check the types? EDIT: No point. Got it. – saruftw Aug 27 '18 at 08:09
  • The reason for my doubt was one of the Pycon talks where the speaker did exactly what's mentioned in the question. Weird. – saruftw Aug 27 '18 at 08:17
  • 3
    @saruftw Please check updated answer. In short - the point of the `typing` module (and in `@overload` in particular) is to allow 3rd party tools (e.g. mypy) to run static checks – awesoon Aug 27 '18 at 08:23
  • Got it. Thanks for explaining. – saruftw Aug 27 '18 at 08:25
  • 3
    Why not just use `Union[str, int]` to note that the function can get either an int or an str? – Corel Jun 21 '21 at 12:47
  • 2
    There are 2 overloads in the second example: `(int) -> int` and `(str) -> str`, this cannot be done with `Union` – awesoon Jun 21 '21 at 13:18
7
# tested in Python 3.8.5 32-bit
# overloads the method
# imports libraries from the base distribution 
# confuses some linters
# undermines type-hinting by documenting *kwargs or dispatch signature

from functools import singledispatch

class Car:
    def __init__(self, color: str, brand: str) -> None:
        self.color = color
        self.brand = brand


@singledispatch
def describe_car(color: str, kind: str) -> str:
    return "Little " + color + " " + kind

@describe_car.register(Car)
def _(car: Car) -> str:
        return describe_car(car.color, car.brand)


newcar = Car("red", "corvette")

print(describe_car("green", "pinto"))
print(describe_car(newcar))

pythonout>
Little green pinto
Little red corvette

whecks
  • 79
  • 1
  • 2