False positive reportCallIssue for generic constructor call using inferred private instance attribute
- Dominant language
- Python
- Stars
- 15.6k
- Forks
- 1.8k
- Avg merge
- 12h 13m
- Merged PRs (30d)
- 52
Description
## Environment data
- Pylance version: 2026.1.102
- OS and version: Windows 10, version 10.0.19044.0
- Python version (& distribution if applicable, e.g. Anaconda): CPython 3.13.4
## Code Snippet
```python
from __future__ import annotations
from abc import ABC
from asyncio import AbstractEventLoop, get_event_loop
from typing import Any, Optional, Type
class Item:
...
class Handler[P: Pool, I: Item, Opt: Any](ABC):
def __init__(self, **kwargs: Any):
...
class Queue[P: Pool, I: Item, Opt: Any]:
def __init__(self, handler: Handler[P, I, Opt], loop: AbstractEventLoop):
self.handler = handler
self.loop = loop
class Pool[I: Item, Opt: Any]:
def __init__(
self,
loop: Optional[AbstractEventLoop],
handler_cls: Type[Handler[Pool[I, Opt], I, Opt]],
**handler_kwargs: Any,
):
self._loop = loop or get_event_loop()
# OK
self._handler = handler_cls(**handler_kwargs)
self.q1: Queue[Pool[I, Opt], I, Opt] = Queue(self._handler, self._loop)
# OK
self.handler = handler_cls(**handler_kwargs)
self.q2 = Queue[Pool[I, Opt], I, Opt](self.handler, self._loop)
# OK
self.handler = handler_cls(**handler_kwargs)
self.q3 = Queue[Pool[I, Opt], I, Opt](handler=self.handler, loop=self._loop)
# False positive
self._handler = handler_cls(**handler_kwargs)
self.q4 = Queue[Pool[I, Opt], I, Opt](self._handler, self._loop)
# False positive
self._handler = handler_cls(**handler_kwargs)
self.q5 = Queue[Pool[I, Opt], I, Opt](handler=self._handler, loop=self._loop)
```
## Repro Steps
1. Open the file in VS Code with Pylance enabled.
2. Use Python 3.13+ so the PEP 695 generic syntax is recognized.
3. Inspect the diagnostics for the five constructor call variants in `Pool.__init__`.
4. Observe that `q1`, `q2`, and `q3` are accepted, but `q4` and `q5` are reported as errors.
5. If needed, comment out the accepted cases and leave only `q4` or `q5` to reproduce the false positive in isolation.
6. Re-introduce any accepted case (`q1`, `q2`, or `q3`) in the same scope and observe that the false positive can disappear.
## Expected behavior
All five constructor calls should be accepted.
The following accepted cases and rejected cases are semantically equivalent in terms of the argument types being passed:
```python
self._handler = handler_cls(**handler_kwargs)
self.q1: Queue[Pool[I, Opt], I, Opt] = Queue(self._handler, self._loop)
self.handler = handler_cls(**handler_kwargs)
self.q2 = Queue[Pool[I, Opt], I, Opt](self.handler, self._loop)
self.handler = handler_cls(**handler_kwargs)
self.q3 = Queue[Pool[I, Opt], I, Opt](handler=self.handler, loop=self._loop)
self._handler = handler_cls(**handler_kwargs)
self.q4 = Queue[Pool[I, Opt], I, Opt](self._handler, self._loop)
self._handler = handler_cls(**handler_kwargs)
self.q5 = Queue[Pool[I, Opt], I, Opt](handler=self._handler, loop=self._loop)
```
Pylance should treat `self.handler` and `self._handler` consistently here, and all calls should use the declared `Queue.__init__(handler, loop)` signature.
In particular, these two cases should be accepted the same way as the corresponding `self.handler` forms:
```python
self.q4 = Queue[Pool[I, Opt], I, Opt](self._handler, self._loop)
self.q5 = Queue[Pool[I, Opt], I, Opt](handler=self._handler, loop=self._loop)
```
The explicit target-annotation form should also not be required as a workaround:
```python
self.q1: Queue[Pool[I, Opt], I, Opt] = Queue(self._handler, self._loop)
```
## Actual behavior
Pylance accepts these cases:
```python
self.q1: Queue[Pool[I, Opt], I, Opt] = Queue(self._handler, self._loop)
self.q2 = Queue[Pool[I, Opt], I, Opt](self.handler, self._loop)
self.q3 = Queue[Pool[I, Opt], I, Opt](handler=self.handler, loop=self._loop)
```
But reports false positives for these equivalent `self._handler` cases:
```python
self.q4 = Queue[Pool[I, Opt], I, Opt](self._handler, self._loop)
self.q5 = Queue[Pool[I, Opt], I, Opt](handler=self._handler, loop=self._loop)
```
Observed diagnostics:
For `q4`:
```text
Expected 0 positional arguments (reportCallIssue)
```
For `q5`:
```text
No parameter named 'handler'
No parameter named 'loop'
```
This means all of the following matter to the diagnostic result:
- whether the temporary attribute is named `handler` or `_handler`
- whether positional or keyword arguments are used
- whether a known-good equivalent case also exists nearby in the same scope
The behavior appears to be context-sensitive. If a correct equivalent case such as `q1`, `q2`, or `q3` is present in the same method, the false positives for `q4`/`q5` may disappear.
This does not look like a genuine signature mismatch, because the accepted and rejected cases differ only in surface form and instance attribute name, not in the effective runtime argument types.
## Logs
```text
N/A
```
Contributor guide
Assessment
This issue has not been assessed yet.