Files
ruff/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md
David Peter e91e2f49db [red-knot] Trust module-level undeclared symbols in stubs (#17577)
## Summary

Many symbols in typeshed are defined without being declared. For
example:
```pyi
# builtins:
IOError = OSError

# types
LambdaType = FunctionType
NotImplementedType = _NotImplementedType

# typing
Text = str

# random
uniform = _inst.uniform

# optparse
make_option = Option

# all over the place:
_T = TypeVar("_T")
```

Here, we introduce a change that skips widening the public type of these
symbols (by unioning with `Unknown`).

fixes #17032

## Ecosystem analysis

This is difficult to analyze in detail, but I went over most changes and
it looks very favorable to me overall. The diff on the overall numbers
is:
```
errors: 1287 -> 859 (reduction by 428)
warnings: 45 -> 59 (increase by 14)
```

### Removed false positives

`invalid-base` examples:

```diff
- error[lint:invalid-base] /tmp/mypy_primer/projects/pip/src/pip/_vendor/rich/console.py:548:27: Invalid class base with type `Unknown | Literal[_local]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/tornado/tornado/iostream.py:84:25: Invalid class base with type `Unknown | Literal[OSError]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/mitmproxy/test/conftest.py:35:40: Invalid class base with type `Unknown | Literal[_UnixDefaultEventLoopPolicy]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
```

`invalid-exception-caught` examples:

```diff
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/cloud-init/cloudinit/cmd/status.py:334:16: Cannot catch object of type `Literal[ProcessExecutionError]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/jinja/src/jinja2/loaders.py:537:16: Cannot catch object of type `Literal[TemplateNotFound]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
```

`unresolved-reference` examples


7a0265d36e/cloudinit/handlers/jinja_template.py (L120-L123)
(we now understand the `isinstance` narrowing)

```diff
- error[lint:unresolved-attribute] /tmp/mypy_primer/projects/cloud-init/cloudinit/handlers/jinja_template.py:123:16: Type `Exception` has no attribute `errno`
```

`unknown-argument` examples


https://github.com/hauntsaninja/boostedblob/blob/master/boostedblob/request.py#L53

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/boostedblob/boostedblob/request.py:53:17: Argument `connect` does not match any known parameter of bound method `__init__`
```

`unknown-argument`

There are a lot of `__init__`-related changes because we now understand
[`@attr.s`](3d42a6978a/src/attr/__init__.pyi (L387))
as a `@dataclass_transform` annotated symbol. For example:

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/attrs/tests/test_hooks.py:72:18: Argument `x` does not match any known parameter of bound method `__init__`
```

### New false positives

This can happen if a symbol that previously was inferred as `X |
Unknown` was assigned-to, but we don't yet understand the assignability
to `X`:


https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/exceptions/handler.py#L90

```diff
+ error[lint:invalid-assignment] /tmp/mypy_primer/projects/strawberry/strawberry/exceptions/handler.py:90:9: Object of type `def strawberry_threading_exception_handler(args: tuple[type[BaseException], BaseException | None, TracebackType | None, Thread | None]) -> None` is not assignable to attribute `excepthook` of type `(_ExceptHookArgs, /) -> Any`
```

### New true positives


6bbb5519fe/tests/tracer/test_span.py (L714)

```diff
+ error[lint:invalid-argument-type] /tmp/mypy_primer/projects/dd-trace-py/tests/tracer/test_span.py:714:33: Argument to this function is incorrect: Expected `str`, found `Literal[b"\xf0\x9f\xa4\x94"]`
```

### Changed diagnostics

A lot of changed diagnostics because we now show `@Todo(Support for
`typing.TypeVar` instances in type expressions)` instead of `Unknown`
for all kinds of symbols that used a `_T = TypeVar("_T")` as a type. One
prominent example is the `list.__getitem__` method:

`builtins.pyi`:
```pyi
_T = TypeVar("_T")  # previously `TypeVar | Unknown`, now just `TypeVar`

# …

class list(MutableSequence[_T]):
    # …
    @overload
    def __getitem__(self, i: SupportsIndex, /) -> _T: ...
    # …
```

which causes this change in diagnostics:
```py
xs = [1, 2]
reveal_type(xs[0])  # previously `Unknown`, now `@Todo(Support for `typing.TypeVar` instances in type expressions)`
```

## Test Plan

Updated Markdown tests
2025-04-23 19:31:14 +02:00

7.4 KiB

Eager scopes

Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved immediately. This is in contrast to (for instance) function scopes, where those references are resolved when the function is called.

Function definitions

Function definitions are evaluated lazily.

x = 1

def f():
    reveal_type(x)  # revealed: Unknown | Literal[2]

x = 2

Class definitions

Class definitions are evaluated eagerly.

def _():
    x = 1

    class A:
        reveal_type(x)  # revealed: Literal[1]

        y = x

    x = 2

    reveal_type(A.y)  # revealed: Unknown | Literal[1]

List comprehensions

List comprehensions are evaluated eagerly.

def _():
    x = 1

    # revealed: Literal[1]
    [reveal_type(x) for a in range(1)]

    x = 2

Set comprehensions

Set comprehensions are evaluated eagerly.

def _():
    x = 1

    # revealed: Literal[1]
    {reveal_type(x) for a in range(1)}

    x = 2

Dict comprehensions

Dict comprehensions are evaluated eagerly.

def _():
    x = 1

    # revealed: Literal[1]
    {a: reveal_type(x) for a in range(1)}

    x = 2

Generator expressions

Generator expressions don't necessarily run eagerly, but in practice usually they do, so assuming they do is the better default.

def _():
    x = 1

    # revealed: Literal[1]
    list(reveal_type(x) for a in range(1))

    x = 2

But that does lead to incorrect results when the generator expression isn't run immediately:

def evaluated_later():
    x = 1

    # revealed: Literal[1]
    y = (reveal_type(x) for a in range(1))

    x = 2

    # The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
    # our inferred type.
    print(next(y))

Though note that “the iterable expression in the leftmost for clause is immediately evaluated” [spec]:

def iterable_evaluated_eagerly():
    x = 1

    # revealed: Literal[1]
    y = (a for a in [reveal_type(x)])

    x = 2

    # Even though the generator isn't evaluated until here, the first iterable was evaluated
    # immediately, so our inferred type is correct.
    print(next(y))

Top-level eager scopes

All of the above examples behave identically when the eager scopes are directly nested in the global scope.

Class definitions

x = 1

class A:
    reveal_type(x)  # revealed: Literal[1]

    y = x

x = 2

reveal_type(A.y)  # revealed: Unknown | Literal[1]

List comprehensions

x = 1

# revealed: Literal[1]
[reveal_type(x) for a in range(1)]

x = 2

# error: [unresolved-reference]
[y for a in range(1)]
y = 1

Set comprehensions

x = 1

# revealed: Literal[1]
{reveal_type(x) for a in range(1)}

x = 2

# error: [unresolved-reference]
{y for a in range(1)}
y = 1

Dict comprehensions

x = 1

# revealed: Literal[1]
{a: reveal_type(x) for a in range(1)}

x = 2

# error: [unresolved-reference]
{a: y for a in range(1)}
y = 1

Generator expressions

x = 1

# revealed: Literal[1]
list(reveal_type(x) for a in range(1))

x = 2

# error: [unresolved-reference]
list(y for a in range(1))
y = 1

evaluated_later.py:

x = 1

# revealed: Literal[1]
y = (reveal_type(x) for a in range(1))

x = 2

# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
# our inferred type.
print(next(y))

iterable_evaluated_eagerly.py:

x = 1

# revealed: Literal[1]
y = (a for a in [reveal_type(x)])

x = 2

# Even though the generator isn't evaluated until here, the first iterable was evaluated
# immediately, so our inferred type is correct.
print(next(y))

Lazy scopes are "sticky"

As we look through each enclosing scope when resolving a reference, lookups become lazy as soon as we encounter any lazy scope, even if there are other eager scopes that enclose it.

Eager scope within eager scope

If we don't encounter a lazy scope, lookup remains eager. The resolved binding is not necessarily in the immediately enclosing scope. Here, the list comprehension and class definition are both eager scopes, and we immediately resolve the use of x to (only) the x = 1 binding.

def _():
    x = 1

    class A:
        # revealed: Literal[1]
        [reveal_type(x) for a in range(1)]

    x = 2

Class definition bindings are not visible in nested scopes

Class definitions are eager scopes, but any bindings in them are explicitly not visible to any nested scopes. (Those nested scopes are typically (lazy) function definitions, but the rule also applies to nested eager scopes like comprehensions and other class definitions.)

def _():
    x = 1

    class A:
        x = 4

        # revealed: Literal[1]
        [reveal_type(x) for a in range(1)]

        class B:
            # revealed: Literal[1]
            [reveal_type(x) for a in range(1)]

    x = 2

x = 1

def _():
    class C:
        # revealed: Unknown | Literal[1]
        [reveal_type(x) for _ in [1]]
        x = 2

Eager scope within a lazy scope

The list comprehension is an eager scope, and it is enclosed within a function definition, which is a lazy scope. Because we pass through this lazy scope before encountering any bindings or definitions, the lookup is lazy.

def _():
    x = 1

    def f():
        # revealed: Unknown | Literal[2]
        [reveal_type(x) for a in range(1)]
    x = 2

Lazy scope within an eager scope

The function definition is a lazy scope, and it is enclosed within a class definition, which is an eager scope. Even though we pass through an eager scope before encountering any bindings or definitions, the lookup remains lazy.

def _():
    x = 1

    class A:
        def f():
            # revealed: Unknown | Literal[2]
            reveal_type(x)

    x = 2

Lazy scope within a lazy scope

No matter how many lazy scopes we pass through before encountering a binding or definition, the lookup remains lazy.

def _():
    x = 1

    def f():
        def g():
            # revealed: Unknown | Literal[2]
            reveal_type(x)
    x = 2

Eager scope within a lazy scope within another eager scope

We have a list comprehension (eager scope), enclosed within a function definition (lazy scope), enclosed within a class definition (eager scope), all of which we must pass through before encountering any binding of x. Even though the last scope we pass through is eager, the lookup is lazy, since we encountered a lazy scope on the way.

def _():
    x = 1

    class A:
        def f():
            # revealed: Unknown | Literal[2]
            [reveal_type(x) for a in range(1)]

    x = 2

Annotations

Type annotations are sometimes deferred. When they are, the types that are referenced in an annotation are looked up lazily, even if they occur in an eager scope.

Eager annotations in a Python file

from typing import ClassVar

x = int

class C:
    var: ClassVar[x]

reveal_type(C.var)  # revealed: int

x = str

Deferred annotations in a Python file

from __future__ import annotations

from typing import ClassVar

x = int

class C:
    var: ClassVar[x]

reveal_type(C.var)  # revealed: Unknown | str

x = str

Deferred annotations in a stub file

from typing import ClassVar

x = int

class C:
    var: ClassVar[x]

reveal_type(C.var)  # revealed: str

x = str