Skip to content

Binder loses narrowed type of a variable if variable may be uninitialized #18619

Closed

Description

@JukkaL

Bug Report

Mypy sometimes loses track of the narrowed type of a variable, at least when the variable can be uninitialized in some code paths. This can happen when variables is both initialized and narrowed in a conditional block.

To Reproduce

if int():
    x: int | str
    x = 0
    x = str(x)
reveal_type(x) # int | str

Expected Behavior

Revealed type is str.

Actual Behavior

Revealed type is int | str.

Discussion

Here are some more realistic examples:

def cond() -> bool:
    return True

if cond():
    x: int | str
    x = 0
    x = str(x)
if cond():
    reveal_type(x) # int | str

for i in [1]:
    y: int | bytes
    y = 0
    y = bytes(y)
reveal_type(y) # int | bytes

def f(x: int | None) -> int:
    return x or 1

def g() -> None:
    if cond():
        if int():
            z = None
        else:
            z = 1
        z = f(z)
    if cond():
        reveal_type(z) # int | None

def h() -> None:
    if cond():
        if int():
            z = 1
        else:
            z = None
        z = f(z)
    if cond():
        reveal_type(z) # int | None

This issue interferes with inferring union types from assignments (#18568).

@ilevkivskyi Do you have an idea of how to fix this, since you did some work on the binder recently? If a variable hasn't been assigned in some conditional code path, maybe the type should be treated as Never when merging types from different code paths?

Activity

  1. ilevkivskyi commented on Feb 6, 2025

    @ilevkivskyi
    Member

    First of all I am far from binder expert :-), but unfortunately I don't think there is an easy/clean fix for this. The problem is that the textually first assignment is the one that sets the declared type. So these two are identical from the point of view of the binder

    if cond():
        x: int | str = ...
        x = 0

    and

    x: int | str = ...
    if cond():
        x = 0

    while for the second one we obviously want to keep the type as union. We would need to track somehow where the variable was first declared and act accordingly. I however don't have any simple/clear idea on this.

  2. ilevkivskyi commented on Feb 6, 2025

    @ilevkivskyi
    Member

    The only relatively clean solution I see is we would need to track in which frames a variable is readable, and ignore frames without information when popping them if a variable is not readable in that frame (now if there is at least one frame without information, we erase all information so far, see update_from_options()). But IIUC this will essentially duplicate the work done in PossiblyUndefinedVariableVisitor.

  3. JukkaL commented on Feb 7, 2025

    @JukkaL
    CollaboratorAuthor

    The only relatively clean solution I see is we would need to track in which frames a variable is readable, and ignore frames without information when popping them if a variable is not readable in that frame

    This looks promising. In my prototype implementation I now update binder on initial assignment, so that if there is nothing in binder, the variable is uninitialized in that frame. This broke a bunch of things, however, but I hope I can fix all the resulting issues. This will possibly be enabled only when the new type inference logic is enabled (inferring unions from assignments) to reduce potential for backward compatibility breaks.

  4. Saunakghosh10 commented on Feb 11, 2025

    @Saunakghosh10

    trying to fix this

  5. JukkaL commented on Feb 11, 2025

    @JukkaL
    CollaboratorAuthor

    @Saunakghosh10 I'm working on this and have a prototype implementation.

  6. self-assigned this
    on Feb 11, 2025
  7. added a commit that references this issue on Mar 19, 2025
    34e8c7c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions