Search code examples
pythontype-hinting

What is the difference between "cast(...)" and "... # type: ..."?


When type-hinting in Python I have two options for marking an expression as certain type:

from typing import cast

foo = cast(str, expression)
bar = expression  # type: str

What's the difference between the two?

I have the impression that sometimes using cast works, while sometimes I have to use the comment. But I have not yet figured out a pattern. Unfortunately I currently have no code-example at hand where one works but not the other.


Solution

  • # type: ignore means "please silence any type-related errors that are emerging from this line".

    cast(TYPE, EXPR) means "I know you think the type of EXPR of X, but I want you to assume the type is actually TYPE, ok?"

    You typically use # type: ignore when you've run into some limitation of the type checker that you can't work around. For example, suppose you try importing some 3rd party library with no stubs or type definitions. In that case, doing:

    # Results in errors like"
    # error: No library stub file for module 'library_with_no_hints'
    import library_with_no_hints
    

    ...would typically result in an error. But you can use # type: ignore to silence that error:

    import library_with_no_hints  # type: ignore
    

    You typically use cast when you have additional out-of-band information the type checker is not aware of that some type X is actually really only Y. For example:

    def parse_config(assume_normalized: bool, thing: List[Union[int, str]]) -> List[int]:
        if assume_normalized:
            # The type-checker thinks that 'thing' is a List[Union[int, str]];
            # we now force it to assume it's really a List[int] instead.
            return cast(List[int], thing)
        else:
            output = []
            for item in thing:
                if isinstance(item, int):
                    output.append(item)
                else:
                    output.append(parse(item))
            return output
    

    You typically see casts when either a) you're mucking around with a lot of deserialization/serialization style code and need to downcast over-broad types into more specific ones, b) you're doing weird things with multiple inheritance, or c) your code is badly designed and you've painted yourself into a corner.

    I personally almost never use casts -- I try and avoid them if I can, and if not, will use at least isinstance checks and the likes so my code will crash at runtime if my assumptions end up being wrong.

    (I also try and ignore # type: ignore, but they sometimes end up being a necessary evil/can help you work around limitations in your type checker.)


    If you plan on using either type-ignores or casts in your codebase, you should also look into configuring your typechecker so it warns you when you're using them in unnecessary places.

    For example, with mypy, you could pass in the --warn-unused-ignores and --warn-unused-casts flags.