Python Forum

Full Version: How to annotation type?
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
There is a simple function.
def func(a: AnyStr, *b: AnyStr) -> Tuple[Optional[AnyStr], AnyStr]:
    for _b in b:
      if a.startswith(_b):
        return (_b, a.removeprefix(_b))
    return (None, a)
Then, I added the ability to convert AValue instances (_b) to str or bytes for some reason.
@dataclass
class AValue:
    value: str
val = AValue("abcd")
def func(a: AnyStr, *b: AnyStr | AValue) -> Tuple[Optional[AnyStr | AValue], AnyStr]:
    for _b in b:
      if isinstance(_b, AValue):
        # ..... some code convert _b into AnyStr (_tb) that match a
      else:
        _tb = _b
      if a.startswith(_tb):
        return (_b, a.removeprefix(_tb))
    return (None, a)
However, type annotations are not perfect. If I write result = func('abcd', 'a', 'b'), type checkers still think result[0] maybe Optional[AnyStr | AValue]. How to annotate result[0] that type checkers will think it's Optional[AnyStr] if there is only AnyStr in b?
a, _ = func("Abcd", AValue("A"), "b", b"c")
reveal_type(a)  # Optional[AValue | str]
b, _ = func("Abcd", "A", b"b")
reveal_type(b)  # Optional[str]
I tried this:
T = TypeVar('T', bytes, str, AValue)

def func(a: AnyStr, *b: T) -> Tuple[Optional[T], AnyStr]:
    for _b in b:
      if isinstance(_b, AValue):
        # ..... some code convert _b into AnyStr (_tb) that match a
      else:
        _tb = _b
      if a.startswith(_tb):
        return (_b, a.removeprefix(_tb))
    return (None, a)
It's not perfect that type checkers will think func("abcd", AValue("efgh"), b"ijkl") (b"ijkl"'s type should be same as "abcd") is ok.
What should I do? (Sorry for my poor english.)
Example with first function,the Tuple[Optional is not needed if use Python 3.10 or newer.
There a two return,so need to address both in Return type.
# pre.py
from typing import AnyStr

def func(a: AnyStr, *b: AnyStr) -> tuple[AnyStr, AnyStr] | tuple[None, AnyStr]:
    for _b in b:
        if a.startswith(_b):
            return _b, a.removeprefix(_b)
    return None, a

a = 'test123'
b = 'test1'
print(func(a, b))
Test using mypy.
G:\div_code\hex
λ mypy pre.py
Success: no issues found in 1 source file

G:\div_code\hex
λ python pre.py
('test1', '23')
So now it work for string and bytes.

Don't know about your converting in last part,would add that in new function that if needed.
Will get less readable if clutter to much in and the addition difficulty to get Type hint to work together.
(May-25-2023, 10:47 AM)snippsat Wrote: [ -> ]Example with first function,the Tuple[Optional is not needed if use Python 3.10 or newer.
There a two return,so need to address both in Return type.
# pre.py
from typing import AnyStr

def func(a: AnyStr, *b: AnyStr) -> tuple[AnyStr, AnyStr] | tuple[None, AnyStr]:
    for _b in b:
        if a.startswith(_b):
            return _b, a.removeprefix(_b)
    return None, a

a = 'test123'
b = 'test1'
print(func(a, b))
Test using mypy.
G:\div_code\hex
λ mypy pre.py
Success: no issues found in 1 source file

G:\div_code\hex
λ python pre.py
('test1', '23')
So now it work for string and bytes.

Don't know about your converting in last part,would add that in new function that if needed.
Will get less readable if clutter to much in and the addition difficulty to get Type hint to work together.

Because I'm trying to make a tcp packet parse system that can parse both str and bytes. All available packet identification is a AValue (for example). If user want to parse a packet, he had to encode himself.
So is it possible??
Not knowing enough about the task and input,but str and bytes can never mix so can eg force convert back and forth.
Now before parse with func do not need AnyStr as ensure_str will always make it str.
Then later can eg convert to bytes again.
# pre.py
def func(a: str, *b: str) -> tuple[str, str] | tuple[None, str]:
    for _b in b:
        if a.startswith(_b):
            return _b, a.removeprefix(_b)
    return None, a

def ensure_str(a, b):
    try:
        a = a.decode()
        b = b.decode()
        return a, b
    except AttributeError:
        return a, b

def ensure_bytes(res: tuple) -> tuple:
    return tuple([s.encode() for s in res])

if __name__ == '__main__':
    a = b'test123'
    b = b'test1'
    a_1, b_1 = ensure_str(a, b)
    print(func(a_1, b_1))
    res = func(a_1, b_1)
    print(ensure_bytes(res))
Test.
G:\div_code\hex
λ mypy pre.py
Success: no issues found in 1 source file

G:\div_code\hex
λ python pre.py
('test1', '23')
(b'test1', b'23')
I finally made it using typing.overload:
@overload
def func(a: AnyStr, *b: AnyStr) -> Tuple[Optional[AnyStr], AnyStr]:
    ...

@overload
def func(a: AnyStr, *b: AValue) -> Tuple[Optional[AValue], AnyStr]:
    ...

@overload
def func(a: AnyStr, *b: Union[AValue, AnyStr]) -> Tuple[Union[AValue, AnyStr, None], AnyStr]:
    ...

def func(a: AnyStr, *b: Union[AValue, AnyStr]) -> Tuple[Union[AValue, AnyStr, None], AnyStr]:
    for _b in b:
      if isinstance(_b, AValue):
        # ..... some code convert _b into AnyStr (_tb) that match a
      else:
        _tb = _b
      if a.startswith(_tb):
        return (_b, a.removeprefix(_tb))
    return (None, a)