-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inner method discards type narrowing done in outer method #2608
Comments
This looks like the same as #2145 which was closed because the binder doesn't keep track of this -- maybe we should reopen that? Or at least keep this one open. |
Let's keep this open. |
(Rising priority to normal, since this appeared third time.) |
How should we do this? I think it's safe to trust the narrower type *if it
prevails over the entire rest of the (outer) function* -- maybe that's
enough? After all the "None" is an artifact of the idiom for mutable
default. (I almost wish there was a way to say "this None isn't really
here" like we have at the class level.)
|
FWIW you can work around this by early-binding And yes, I think it’s safe to trust the narrowed type iff it applies to the entire rest of the outer function. |
+1 also from me. |
(I am raising priority to high since this appears quite often.) |
Can we change the topic of this (important) issue to something more descriptive? |
Hope this helps! |
Ran into this today, here's another minimal example:
|
Another example (with a subclass narrowing) is #4679 |
I'm running into this as well. I tried to work around it by capturing the value from the outer context as the default value for an inner parameter, as @carljm suggested above, but that doesn't help: from typing import Optional
def outer(key: Optional[str]) -> None:
if key:
reveal_type(key)
def inner(key: str = key) -> bool:
return True mypy 0.701 output: (default configuration)
So it seems that mypy is not only cautious when the body of the inner function uses values from the outer function, but it also ignores narrowing when defining the inner function. As far as I know, default parameter values don't need to be treated differently from any other expression evaluation. |
@mthuurne A good point, default arguments at least should use the narrowed type. |
Let me clarify "evaluated in its own scope" and "assigned". The scope of the comprehension includes the global scope (or whatever scope the comprehension is contained in). Assignments include assignment expressions and the variables named in the In this example:
If you (i.e., mypy) are traversing a module, class, or function body, you know that any assignment expression, even if contained in a comprehension or generator, is a binding in that body's scope. |
Yes, I think your analysis and description are correct. |
Question In my copy of mypy 0.931 I see nothing to handle Could you put an |
|
Yes. Let me elaborate.
Updated implementation: |
I have a proof-of-concept implementation that fixes this issue. The idea is to use the narrowed type of a local variable in a nested function if the local variable is not assigned to after the nested function/lambda definition. It still needs polish and it only handles the most basic use cases currently, but it seems promising so far. Hopefully I can get a PR up soon. |
+1 Ran into this today. Having optionally from __future__ import annotations
def foo(lst: list[int] | None = None) -> None:
if lst is None:
lst = []
def append(x: int) -> None:
lst.append(x) # mypy inferred `lst` as `Optional[List[int]]`
append(1) I also tried from __future__ import annotations
from typing import cast
def foo(lst: list[int] | None = None) -> None:
if lst is None:
lst = []
lst = cast(list[int], lst) # redundant-cast
def append(x: int) -> None:
nonlocal lst # won't work
lst.append(x) # mypy inferred `lst` as `Optional[List[int]]`
append(1) |
Fixes #2608. Use the heuristic suggested in #2608 and allow narrowed types of variables (but not attributes) to be propagated to nested functions if the variable is not assigned to after the definition of the nested function in the outer function. Since we don't have a full control flow graph, we simply look for assignments that are textually after the nested function in the outer function. This can result in false negatives (at least in loops) and false positives (in if statements, and if the assigned type is narrow enough), but I expect these to be rare and not a significant issue. Type narrowing is already unsound, and the additional unsoundness seems minor, while the usability benefit is big. This doesn't do the right thing for nested classes yet. I'll create an issue to track that. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
FYI: I run into this with following example: def handle_now(now: datetime.datetime | None = None) -> ...:
if now:
class Signer(TimestampSigner):
def get_timestamp(self) -> float:
return now.timestamp()
else:
Signer = TimestampSigner
... In this case it is of course not possible to change the signature of get_timestamp(). |
@bast1aan mypy does have Final, but doesn't seem like type inference takes it into the account in case of nested classes, so you'll still need to do an extra That said,
if you run it, it will print 2 because |
Hello, I'm so sorry to comment on a closed issue. I was also puzzled by this error when I tried something like this: import os
import requests
from dotenv import load_dotenv
load_dotenv()
URL = os.getenv("URL")
assert URL is not None
def ping() -> bool:
try:
requests.get(URL) # Error: Argument 1 to "get" has incompatible type "str | None"; expected "str | bytes" [arg-type]
return True
except requests.exceptions.ConnectionError:
return False I thought the The workaround that makes the error go away is: URL = os.getenv("URL")
assert URL is not None
URL = URL Thank you. |
So everybody knows not to write
def foo(arg={'bar': 'baz'}):
So instead one usually writes something like:
Now add some types:
So all is good, unless you try to create an inner method:
I guess there are lots of workarounds (assign to temporary variable, pull out the inner method, pass arguments into the inner) but they all require editing existing code which is always scary. Hmm. I guess the easiest thing is to just add an
assert arg is not None
in the inner. That's not too intrusive, anyway thought you might want to know.The text was updated successfully, but these errors were encountered: