Replies: 5 comments 6 replies
-
As you mentioned, mypy doesn't track unbound variables or implement any checks in this area. If you feel that this diagnostic check isn't providing enough value, you can disable it in pyright (reportUnboundVariable = none), and it will act more like mypy in this respect. If you do find value in unbound variable checking, then my recommendation is to write your code in a way where variables are provably bound in all code paths. Python is unlike most programming languages in that it allows local variables to appear within a scope but have no value bound to them. In all other languages I can think of (C, C++, Javascript, TypeScript, Java, C#, Basic, Julia, etc.), you would be forced to assign a value to a variable on all code paths, and you would receive a compiler/interpreter error if you failed to do so. So if you adapt your usage of Python to be like all other programming languages in this respect, you won't run into any problems with unbound variable errors. So I guess I look at this not as a "workaround" but as the preferred way to write robust, bug-free code — something that other programming languages would enforce but Python does not. |
Beta Was this translation helpful? Give feedback.
-
For what it's worth, I did implement this sort of logic in my company's typechecker, pyanalyze. There is code in https://github.com/quora/pyanalyze/blob/ae4292f9b3c02616912712551d08e8bff3eb69aa/pyanalyze/name_check_visitor.py#L2910 for figuring out how many elements are in an iterator. It was enough to make our possibly unbound variable check work without too many false positives, but the code is definitely not completely sound. |
Beta Was this translation helpful? Give feedback.
-
I'm running into a different version of this, where I have multiple related variables which get bound together but which all may be unbound if an exception is raised. The code pattern looks something like: try:
x, y, z = somefunc()
except ValueError:
if should_ignore:
x = None
else:
raise
if x is not None:
someotherfunc(x, y, z) Pyright complains (rightfully) that y and z are potentially unbound, but in practice their use is guarded by the check on x, so any time we end up ignoring the exception the code will skip the call to someotherfunc() that uses x, y, and z. I don't expect a static analyzer to figure this out, but I'm curious of people's thoughts on other ways to structure this code, as I'd rather not assign dummy values to all of x, y, and z here. As pointed out above, that could have ripple effects if some of the types have non-trivial constructors which don't have any obvious dummy value which can be assigned, and making the type of those other objects Optional to allow the use of Any thoughts? |
Beta Was this translation helpful? Give feedback.
-
My workaround is to use the async for retry in AsyncRetrying(stop=stop_after_attempt(3)):
with retry:
response = await api_client.foobar()
break
else:
assert False
# response is no longer `Unbound | ...` Regardless, I am a bit surprised to find out that |
Beta Was this translation helpful? Give feedback.
-
For those coming across this thread, this is the most elegant solution I've found: x: int = None # pyright: ignore
y: str = None # pyright: ignore
done = False
while not done:
x, y, done = foo(...)
bar(x, y) |
Beta Was this translation helpful? Give feedback.
-
This has been discussed before as an issue, but I want to propose more general discussion about it.
As a programmer, I can easily see that the Pyright error about
x
being unbound is not useful, because reading this code I know that the for-loop body will be executed at least once.Given the nature of Python, though, it's impossible in general to determine if an iterator will yield at least one value, or stop before the first iteration. A similar form of this has been discussed in #844 and marked as "as designed" with the following motivation:
As a workaround, I can convince Pyright that a binding of
x
exists in all cases, e.g. by setting it before the loop. Unfortunately this might have other consequences:In this example I could have initialized the variable with an int; but some types have non-trivial constructors. Using
None
allows me to save the day with an assertion:The workaround is good enough, and possibly preferable to a
# type: ignore
annotation which would silence all checks, and need to be repeated for every usage ofx
. I still feel it's awkward, because it adds noise (for a reader, the assertion is not adding information, and can be confusing), forces code to adapt to the logic of the type checker, and introduces runtime behavior.I would like to discuss this more in general. Is there a way to deal with this, or should this be considered a necessary shortcoming of type checkers?
For comparison: Mypy doesn't have an equivalent check. Pytype doesn't seem to complain either. Pyright is the strictest about this, which I like in general. But it also means that only Pyright has an incentive to care about this.
Brainstorming some possible approaches:
Include information in the type system about iterators that yield at least once. This sounds hard, and would not cover all cases. I see it working with some generator functions and not others. In the
range()
case the behavior depends on the constructor. For collections, it depends on content (tracking an empty collection is also possible in theory but doesn't cover all cases and is hard).Annotate loops whose body is certain to be run at least once. After all, maybe this is more about control flow than it is about iterators. If the annotation is a comment, it doesn't have runtime cost. I don't know precedents for this and it feels a bit inelegant. But also potentially more informative than the assertion in the above workaround.
count()
,repeat()
, and possibly a couple more types are common enough to cover many use cases. I see the value, but also the slippery slope argument.I don't care particularly for any of the above solutions. Pragmatic arguments could be made for most points (excluding 1 maybe?) if many users need this, but I'm not strongly convinced any is significantly better than the status quo.
Anything else?
Extra question for fun: can Python change in a way to make this easier? What language change would enable this? It sounds remote right now, but there is a lot of ongoing work on static analysis and compilation. Is there a change that could make these efforts more valuable? This is currently not within Mypy's scope, but it could be if the language (or the optional typing system) made this possible.
Beta Was this translation helpful? Give feedback.
All reactions