Incompatible types in assignment when casting function argument (original) (raw)
from typing import *
JsonValue = None | bool | int | float | str
JsonObject = dict[str, 'JsonData']
JsonArray = list['JsonData']
JsonData = JsonValue | JsonObject | JsonArray
def parse(data: JsonData):
... # JSONschema validation here
data = cast(list[str] | None, data) # After validation we know that data is list[str] or None
# ^^^ Incompatible types in assignment (expression has type "list[str] | None", variable has type "JsonData")
... # Further data processing
I have a function that takes JSON data as an argument, validates it using JSONschema and parsing the given configuration after validation. I’m using typing.cast
to tell type checker (mypy) that data is now list[str]
or None
. Mypy reports an error: Incompatible types in assignment (expression has type "list[str] | None", variable has type "JsonData")
. How to cast the data
variable here correctly?
t1m013y (Timofey Fomin) June 12, 2025, 9:56am 2
Found a way to get rid of the typing error by casting to a new variable, but can it be done without new variable creation (cast to the same variable)?
ferdnyc (FeRD (Frank Dana)) June 12, 2025, 10:36am 4
Unlikely, because the issue is that you declared the type of data
in your function signature: data: JsonData
says that the data
object is expected to have that type.
typing.cast
is meant to be used in the other direction, e.g. you have a variable declared to take JsonData
and you want to assign something that isn’t a JsonData
into it, because you know that’s safe to do, then you could write:
data = cast(JsonData, my_var_that_mypy_knows_holds_a_string_list)
…and mypy will let that through without complaint, which it wouldn’t if you just wrote:
data = my_var_that_mypy_knows_holds_a_string_list
…when data
is annotated as holding JsonData
.
But changing a variable’s type mid-stream breaks the static analysis, not sure if there’s a mypy-approved way of doing that.
t1m013y (Timofey Fomin) June 12, 2025, 10:44am 5
Thanks for your answer.
Will it be mypy-approved to assign casted value to new name, e.g. like the code below?
data_validated = cast(list[str] | None, data)
ferdnyc (FeRD (Frank Dana)) June 12, 2025, 11:00am 6
Oh, sure, because then you’re effectively declaring the type of data_validated
with the cast()
. It’s the same as writing,
data_validated: list[str] | None
data_validated = cast(list[str], data)
(Assuming data
actually does hold a list[str]
.)
…Of course, you have to be careful, because you’re overriding mypy’s type-checking, and if you lie to it it’ll believe you. This will also pass mypy without complaint, despite being completely false:
int_list = [1, 2, 3, 4, 5]
data_validated: list[str] | None
data_validated = cast(list[str], int_list)
…and then you’ll get a runtime error if you try something like,
if data_validated is not None:
print(" ".join(data_validated))
InSync (InSync) June 12, 2025, 1:09pm 7
This is one of the most common question about Python’s type system: Incompatible types in assignment (expression has type “List[str]”, variable has type “List[Union[str, int]]”) (I can’t find a more highly upvoted post).
The reason is that list
is invariant, and so list[str]
is not assignable to list[JsonData]
, which expands to list[None | str | bool | int | ...]
.
mart-r (Mart Ratas) June 13, 2025, 11:07am 8
It’s hard to tell from the small example you gave, but my best guess is that there’s some complex validation and if/else statements within your method.
If that’s the case, here’s what I would do:
def is_valid_list_of_strings(data: JsonData) -> bool:
return True # add logic, of course
def parse_list_of_strings(data: list[str]):
pass # do the parsing, of course
def parse(data: JsonData):
if is_valid_list_of_strings(data):
return parse_list_of_strings(cast(list[str], data))
# other conditions similarly
I.e create a separate method that deals with the specific type you’ve validated. This should satisfy mypy
because you’re not re-assigning the value to the existing variable.
t1m013y (Timofey Fomin) June 13, 2025, 1:17pm 10
Validation uses JSON schema, if the validation fails, the exception is raised, if it’s valid, the code continues to the parse section.
mart-r (Mart Ratas) June 13, 2025, 1:46pm 11
If you’re only expecting to use a specific type of data (list[str]
), why would you allow a broader type in the function signature?
If your function only works with list[str]
as an argument, then it should be typed as such.
Otherwise it’s like telling the waiter to choose any meal for you and then refusing to pay when they bring you a beef burger because you expected a vegan meal.
The function signature should be as broad as possible, but as narrow as needed. If the method can’t handle anything other than list[str]
then that’s what’s needed.
t1m013y (Timofey Fomin) June 13, 2025, 2:11pm 12
The method is defined to take JsonData as the first argument in the ABC, so it will break Liskov Substitution Principle.
Also user may pass invalid configuration to the argument (list element strings format is limited by regex), so an exception can be raised anyway (even if the argument was annotated as list[str]
)
mart-r (Mart Ratas) June 13, 2025, 4:07pm 13
There was nothing in the initial post (or up to now) that would indicate you were dealing with a bound method or inheritence. You just provided an example function.
As I’m sure you’re aware, type hints are generally just decorative. Exceptions may be raised at runtime anyway since no types are actually guaranteed.
You brought up Liskov Substitution Principle. I think the whole problem here is that your derived class is (effectively) breaking it already. You just want to fool static checkers (such as mypy
) into thinking it’s not.
Now, the trivial solution would be to just use cast
as you did in the OP and set it to a different variable and document the raise exception in the doc string for the base class (in a general manner, of course). As pointed out above, you can’t just overwrite the type of the existing one.
But if you want to have all your types line up nicely, you may be able to use generics to have each implementation explicitly state what it can and cannot parse.
t1m013y (Timofey Fomin) June 13, 2025, 4:30pm 14
I made it to simplify the example. The question was about casting existing variable, not about changing annotations of the function (method) argument.
Yes, because type checker does not take JSON schema validation into account. Also I need this function to take any variable of JsonData
type without type errors, validating it inside function.
mart-r (Mart Ratas) June 13, 2025, 7:28pm 15
You’re absolutely right! This was a question about typing. And I lost (missed?) the scope!
So it sounds like you’d like is a sort of isinstance(data, (list[str], None))
method. Because generally an instanceof
check will narrow down the type of the same variable neatly and in an acceptable manner.
So best I can think of would be something like this:
def parse(data: JsonData):
if not (data is None or (isinstance(data, list) and
(not data or isinstance(data[0], str)))):
raise ValueError
parts: list[str] | None = data
return parts # deal with it, separately, of course
But this doesn’t quite work because mypy
doesn’t fully understand the contents of the list
.
So best I can tell, you’ll still need to do a cast
and assign to another variable. It’s probably fine. I’d probbaly leave a # NOTE
next to it as to why that is necessary.
beauxq (Doug Hoskisson) June 13, 2025, 8:05pm 16
If you use a good JSON schema validator, like pydantic or msgspec, then the type checker can take JSON schema validation into account.