How do I annotate a function that takes AnyStr with a str default value?

Question:

I wish to type-annotate a function that takes an AnyStr argument that defaults to a str and also returns an AnyStr of the same type. However, if I write this:

from typing import AnyStr

def func(s: AnyStr = ".") -> AnyStr:
    return s

then mypy fails with "Incompatible default for argument "s" (default has type "str", argument has type "bytes")".

I also tried splitting the code into a .py and .pyi file like so:

.py file:

def func(s = "."):
    return s

.pyi file:

from typing import AnyStr

def func(s: AnyStr = ...) -> AnyStr:
    ...

… but I must be invoking mypy wrong, because it fails to type-check the invocations of func; e.g., if I add func(42) to the .py file, mypy doesn’t complain.

What is the correct way to annotate my function and get the code to be completely type-checked?

Asked By: jwodder

||

Answers:

Use None as the default value, then inside the function use two casts: one to replace None with "." cast as an Optional[AnyStr], and one to cast the return value as an AnyStr.

def func(s : AnyStr = None) -> AnyStr:
    if s is None:
        s = cast(Optional[AnyStr], ".")
    return cast(AnyStr, s)

EDIT: My original answer fails, as the type check ignores the concrete implementation:

One possibility is to use overload to enumerate the two cases covered by AnyStr:

from typing import overload


@overload
def func(s: str = ".") -> str:
    pass


@overload
def func(s: bytes) -> bytes:
    pass


def func(s):
    return s 
Answered By: chepner

This case was discussed here. Guido’s answer in particular:

I think the problem here is that in general, e.g. if there are other parameters also using _T in their type, the default value won’t work. Since this is the only parameter, you could say that this is overly restrictive, and if there are no parameters, the return type should just be determined by that default value (in your simplified example, you’d want it to return int). But I’m not so keen on doing that, since it breaks as soon as another generic parameter is used.

Based on that thread, you can use the following workaround:

from typing import AnyStr, overload


@overload
def func() -> str: ...

@overload
def func(s: AnyStr) -> AnyStr: ...

def func(s = "."):  # unchecked implementation
    return func_internal(s)

def func_internal(s: AnyStr) -> AnyStr:
    # a checked complicated body moved here
    return s
Answered By: alex_noname

Perhaps this is what you want:

@overload
def func(s: str=...) -> str: ...   
@overload
def func(s: bytes) -> bytes: ...
@overload
def func(s: None) -> str: ...    

def func(s="."):
    return s


r1 = func()
reveal_type(r1)
r2 = func("1")
reveal_type(r2)
r3 = func(b"1")
reveal_type(r3)

Mypy: The stdout of the command line is:
note: Revealed type is 'builtins.str'
note: Revealed type is 'builtins.str'
note: Revealed type is 'builtins.bytes'
Answered By: hussic