microsoft / microsoft/pyright

False positive reportCallIssue for generic constructor call using inferred private instance attribute

Open
#11,356 0 comments 0 reactions 1 assignee Claimed by @bschnurr View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.