overload following optional argument

Viewed 719

I have a class Animal with a method foo which has different return types according to a boolean parameter inplace which follows an optional parameter bar. I'd like to overload the function so that the return type is known if the value of inplace is known

Here's my code:

# main.py

from __future__ import annotations

from typing import Optional, overload, Literal 


class Animal:
    @overload
    def foo(self, bar=..., inplace: Literal[False]=...) -> Animal:
        ...

    @overload
    def foo(self, bar=..., inplace: Literal[True]=...) -> None:
        ...

    def foo(
        self, bar=None, inplace: bool = False
    ) -> Optional[Animal]:
        ...


reveal_type(Animal().foo(bar='a'))
reveal_type(Animal().foo(inplace=True))
reveal_type(Animal().foo(inplace=False))
$ mypy main.py
main.py:8: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
main.py:21: note: Revealed type is 'main.Animal'
main.py:22: note: Revealed type is 'None'
main.py:23: note: Revealed type is 'main.Animal'
Found 1 error in 1 file (checked 1 source file)

https://mypy-play.net/?mypy=latest&python=3.9&gist=49da369f6343543769eed2060fa61639

How can I avoid the Overloaded function signatures 1 and 2 overlap with incompatible return types error on line 8?

3 Answers

This seems to work:

from __future__ import annotations

from typing import Optional, overload, Literal 


class Animal:

    # using defaults
    @overload
    def foo(self, bar=..., inplace: Literal[False]=...) -> Animal: ...

    # using inplace = True
    
    # with bar
    @overload
    def foo(self, bar, inplace: Literal[True]) -> None: ...

    # without bar
    @overload
    def foo(self, *, inplace: Literal[True]) -> None: ...

    # with bool
    @overload
    def foo(self, bar=..., inplace: bool=...) -> Optional[Animal]: ...

    def foo(
        self, bar=None, inplace = False
    ):
        ...


reveal_type(Animal().foo(bar='a'))
reveal_type(Animal().foo(bar='a', inplace=True))
reveal_type(Animal().foo(bar='a', inplace=False))
reveal_type(Animal().foo(inplace=True))
reveal_type(Animal().foo(inplace=False))
reveal_type(Animal().foo())

inplace: bool
reveal_type(Animal().foo(bar='a', inplace=inplace))
reveal_type(Animal().foo(inplace=inplace))

Lots of overloads, but perhaps that's inevitable here

The following issue on Mypy is extremely related: https://github.com/python/mypy/issues/5486

# main.py

from __future__ import annotations

from typing import Literal, Optional, overload


class Animal:
    @overload
    def foo(self, bar=..., inplace: Literal[False] = ...) -> Animal:
        ...

    @overload
    def foo(self, bar=..., *, inplace: Literal[True]) -> None:
        ...

    def foo(self, bar=None, inplace: bool = False) -> Optional[Animal]:
        ...


reveal_type(Animal().foo(bar="a"))
reveal_type(Animal().foo(inplace=True))
reveal_type(Animal().foo(inplace=False))
$ mypy main.py 
main.py:21: note: Revealed type is "main.Animal"
main.py:22: note: Revealed type is "None"
main.py:23: note: Revealed type is "main.Animal"

By adding a *, before the inplace argument on the second overload, and removing its default value, it does work as expected. This was suggested on this comment.

This does, however, introduce an issue if you attempt to call the function using positional arguments:

reveal_type(Animal().foo("a", True))
main.py:25: error: No overload variant of "foo" of "Animal" matches argument types "str", "bool"
main.py:25: note: Possible overload variants:
main.py:25: note:     def foo(self, bar: Any = ..., inplace: Literal[False] = ...) -> Animal
main.py:25: note:     def foo(self, bar: Any = ..., *, inplace: Literal[True]) -> None
main.py:25: note: Revealed type is "Any"
Found 1 error in 1 file (checked 1 source file)

This issue is mentioned on another comment, and it's suggested that adding another overload could fix it.

Try:

@overload
def foo(self, inplace: Literal[False]=..., bar=...) -> Animal:
    ...

@overload
def foo(self, inplace: Literal[True], bar=...,) -> None:
    ...

def foo(self, inplace=False, bar=None):
    ...

I changed order of args else second overload should not be correct.

Related