Search code examples
pythonmypy

Are there any alternative to this overload case in python with typing?


currently I am implementing a lot of subclasses that should implement a function, this is a minimal example of the current project.

In this case, I have a function that needs to call, to the login of a user, but depending of the implementation injected previously the application will use UserUP or UserToken

Because in login_user I have all data available for the two options, I set the params like follow.

from typing import Protocol, runtime_checkable

@runtime_checkable
class User(Protocol):
    def login(self, **kwargs) -> None:
        raise NotImplementedError


class UserUP(User):
    def login(self, user: str, password:str, **kwargs) -> None:
        print(user)
        print(password)


class UserToken(User):
    def login(self, token:str, **kwargs) -> None:
        print(token)


def login_user(user: User) -> None:
    user.login(user="user", password="password", token="Token")

login_user(UserUP())
login_user(UserToken())

The problem is that when I run mypy I get errors like the following:

app.py:10: error: Signature of "login" incompatible with supertype "User"  [override]
app.py:10: note:      Superclass:
app.py:10: note:          def login(self, **kwargs: Any) -> None
app.py:10: note:      Subclass:
app.py:10: note:          def login(self, user: str, password: str, **kwargs: Any) -> None
app.py:16: error: Signature of "login" incompatible with supertype "User"  [override]
app.py:16: note:      Superclass:
app.py:16: note:          def login(self, **kwargs: Any) -> None
app.py:16: note:      Subclass:
app.py:16: note:          def login(self, token: str, **kwargs: Any) -> None

Of course, the signature is incompatible, but which alternatives do I have to implement things like this?


Solution

  • Use keyword_only marker in all signatures and describe all possible parameter in User.login

    You need to expose which possible keywords can be used, and which type, in the main function.

    from typing import Protocol, runtime_checkable
    
    
    @runtime_checkable
    class User(Protocol):
        def login(self, *, user: str, password: str, token: str, **kwargs) -> None:
            raise NotImplementedError
    
    
    class UserUP(User):
        def login(self, *, user: str, password: str, **kwargs) -> None:
            print(user)
            print(password)
    
    
    class UserToken(User):
        def login(self, *, token: str, **kwargs) -> None:
            print(token)
    
    
    def login_user(user: User) -> None:
        user.login(user="user", password="password", token="Token")
    
    
    login_user(UserUP())
    login_user(UserToken())
    
    Success: no issues found in 1 source file
    

    Protocol leads to implementation

    SUTerliakov suggested another implementation in the comment section: https://mypy-play.net/?mypy=master&python=3.10&flags=strict&gist=358f3ceba80b1d8141e49d223e25495a

    from typing import Any, Protocol, runtime_checkable, ParamSpec
    
    
    _P = ParamSpec('_P')
    
    
    @runtime_checkable
    class User(Protocol[_P]):
        def login(self, *args: _P.args, **kwargs: _P.kwargs) -> None:
            raise NotImplementedError
    
        
    
    class UserUP(User[str, str]):
        def login(self, user: str, password: str, *args: Any, **kwargs: Any) -> None:
            print(user)
            print(password)
    
    
    class UserToken(User[str]):
        def login(self, token: str, *args: Any, **kwargs: Any) -> None:
            print(token)
    
    
    def login_user(user: User[Any]) -> None:
        user.login(user="user", password="password")
    
    login_user(UserUP())
    login_user(UserToken())
    

    This solution generate a valid mypy report:

    Success: no issues found in 1 source file
    

    There is two caveats with this implementation:

    1. You don't need to update the protocol to pass mypy, which means that you can create new implementation that require specific arguments without mypy telling you that the protocol requires it:

    With the following code, you have a valid mypy check and yet, a crash because token is required by the UserToken.login method.

    def login_user(user: User) -> None:
        user.login(user="user", password="password")
    

    mypy result: Success: no issues found in 1 source file

    Run result:

    TypeError: UserToken.login() missing 1 required positional argument: 'token'
    
    1. You can't trust the protocol. Static type checking won't warns you that some subclasses of User need specific arguments.

    What is the behavior of this answer solution:

    The protocol protect further implementation. If the protocol doesn't state that a specific parameter need to be fulfilled to match any protocol implementation, you can't define an implementation requiring this parameter.

    Let's remove the token argument in our login_user like previously:

    def login_user(user: User) -> None:
        user.login(user="user", password="password")
    

    mypy result:

    file.py:23: error: Missing named argument "token" for "login" of "User"  [call-arg]
    Found 1 error in 1 file (checked 1 source file)