最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How to annotate a function that preserves type but applies a transformation to certain types in Python? - Stack Overflow

programmeradmin6浏览0评论

Let's say I have a function that takes only an argument and is guaranteed to return a value of the same type as the argument. Inside, depending on the specific type of the argument, it will apply some transformations or others.

A minimal example could be something like this:

from typing import TypeVar

T = TypeVar("T")
S = TypeVar('S', bound=str)

def str_transform(s: S) -> S:
    return s

def take_any_transform_only_str(obj: T) -> T:
    if isinstance(obj, str):
        return str_transform(obj)
    return obj

It seems straightforward that take_any_transform_only_str will always return a value of the same type than the argument it was given. And yet, mypy (v1.15.0) complains with:

test.py:11: error: Incompatible return value type (got "str", expected "T")  [return-value]

What would be the correct way to annotate this function?

(Probably I could use typing.overload, but I'd very much prefer a solution based on type variables than having to manually annotate the different cases)

Let's say I have a function that takes only an argument and is guaranteed to return a value of the same type as the argument. Inside, depending on the specific type of the argument, it will apply some transformations or others.

A minimal example could be something like this:

from typing import TypeVar

T = TypeVar("T")
S = TypeVar('S', bound=str)

def str_transform(s: S) -> S:
    return s

def take_any_transform_only_str(obj: T) -> T:
    if isinstance(obj, str):
        return str_transform(obj)
    return obj

It seems straightforward that take_any_transform_only_str will always return a value of the same type than the argument it was given. And yet, mypy (v1.15.0) complains with:

test.py:11: error: Incompatible return value type (got "str", expected "T")  [return-value]

What would be the correct way to annotate this function?

(Probably I could use typing.overload, but I'd very much prefer a solution based on type variables than having to manually annotate the different cases)

Share Improve this question edited Feb 5 at 17:33 InSync 10.5k4 gold badges15 silver badges51 bronze badges asked Feb 5 at 9:20 mgabmgab 3,9945 gold badges20 silver badges32 bronze badges 3
  • Does str_transform modify a Literal? If yes, be careful, your type-checker would infer a different literal than you have at runtime. – Daraan Commented Feb 5 at 11:33
  • Good call, but no, this is a very minimal example but the real case is actually modifying some attributes of obj if it's of a given subtype what T could be – mgab Commented Feb 5 at 12:13
  • In that case I would just slap it with a # type: ignore[return-type] as you know the type does not change. I would also prefer it over cast, as casting is a stronger lie. – Daraan Commented Feb 5 at 13:43
Add a comment  | 

1 Answer 1

Reset to default 2

mypy does not track the relationship between the types of symbols. Unfortunately, it means that it is unable to detect that T is str or a subclass of str in the isinstance() branch of your function.

If you do not want to use overload, then you could try a typing.cast(). It is a useful escape hatch when you can determine something that a type checker is unable to. The type checker just takes your word that type safety is satisfied. At runtime, cast() is an identity function that returns its second argument without performing any checks. As such, using cast() will mask any errors should the return type of str_transform() be changed. This means using typing.overload is safer, if more verbose.

Example usage:

return typing.cast(T, str_transform(obj))

Using overload

Using overload is the safer alternative as it means mypy can alert you if the return type of str_transform() changes in a way that is incompatible with take_any_transform_only_str(). However, it requires a somewhat strange overload to work as desired.

@overload
def take_any_transform_only_str(obj: S) -> S:
    # Using `S` over `str` in the overload means mypy will
    # preserve the type of subclasses of `str`
    pass
@overload
def take_any_transform_only_str(obj: T) -> T: pass
def take_any_transform_only_str(obj: T | str) -> T | str:
    # `| str` is an escape hatch to allow returning `str` when the type
    # of` obj` has been narrowed to `str`. mypy would complain even if
    # the return type was `S`, as `S` is not the same as `str`
    if isinstance(obj, str):
        return str_transform(obj)
    else:
        return obj

This preserves the types as desired: eg.

class MyStr(str):
    pass

reveal_type(take_any_transform_only_str(123))
# note: Revealed type is "int"
reveal_type(take_any_transform_only_str(''))
# note: Revealed type is "str"

# Shows that types which are subclasses of str are preserverd
reveal_type(take_any_transform_only_str(MyStr()))
# note: Revealed type is "MyStr"

# Also shows that complex types are preserved and allowed
x: int | str = ''
reveal_type(take_any_transform_only_str(x))
# note: Revealed type is "Union[int, str]"
发布评论

评论列表(0)

  1. 暂无评论