2025, Dec 08 23:00

Resolve Pylance '__getitem__' Errors in pyAlex with Python Type Narrowing: cast, isinstance, assert

Getting '__getitem__' errors from Pylance with pyAlex? Learn why untyped unions confuse the checker and how to fix them using casts, isinstance, and asserts.

Static type checkers shine when libraries are well-annotated. When they are not, even code that runs perfectly can light up your IDE with red squiggles. That is exactly what happens with pyAlex and Pylance: you can look up an institution, access its dict-like fields at runtime, and still get an error about __getitem__ during static analysis.

What the setup looks like

pyAlex models entities as dict subclasses. The relevant parts are straightforward:

class OpenAlexEntity(dict):
    """Base class for OpenAlex entities."""

    pass


class Institution(OpenAlexEntity):
    """Class representing an institution entity in OpenAlex."""

    pass

Fetching an institution works as expected at runtime. Here is a compact example that reproduces the Pylance complaint while still running fine:

from pyalex import Institutions

hits = Institutions().search("MIT").get()
first_item = hits[0]
print(type(first_item))  # prints "Institution"
print(first_item["display_name"])  # prints "Massachusetts Institute of Technology"

Pylance reports an error for the subscripting line, for example:

No overloads for "__getitem__" match the provided arguments Pylance reportCallIssue
builtins.pyi(1062, 9): Overload 2 is the closest match

Why the warning appears

The root cause is that pyAlex is not typed. Without type hints, the checker must infer shapes from usage, which often leads to wide unions and Any | Unknown. In this specific call chain, Institutions.get can return multiple shapes, and there is no overload describing the difference between one vs. many results. The inferred type of hits therefore becomes a union that includes list-like types. After indexing with [0], first_item is still a union that, from the checker’s perspective, can be a list rather than a dict subclass. Lists do not support string-based __getitem__, hence the diagnostic.

Concretely, the types can end up as:

hits: tuple[OpenAlexResponseList | Any, Unknown | Any | None] | OpenAlexResponseList | Any
first_item = hits[0]
first_item: OpenAlexResponseList | Any | Unknown
first_item["some_string"]  # -> error: list.__getitem__(self, str) is not valid

At runtime, print confirms first_item is an Institution. But static analysis does not execute code, and without precise annotations it cannot narrow the union by itself.

How to fix it in user code

When you know the correct type, tell the checker. You can do this with a cast, a runtime isinstance guard, or an assert that both narrows the type and fails loudly if your assumption is wrong at runtime.

Cast when you are certain and want a concise fix:

from typing import cast
from pyalex import Institutions, Institution

records = Institutions().search("MIT").get()
entity = cast("Institution", records[0])
print("Found:", entity["display_name"])  # OK for both runtime and Pylance

Use a guard if you want the checker to narrow based on a runtime check:

from pyalex import Institutions, Institution

bundle = Institutions().search("MIT").get()
candidate = bundle[0]

if isinstance(candidate, Institution):
    print(type(candidate))
    print(candidate["display_name"])  # safe

Assert your expectation when you prefer a hard guarantee and immediate failure if it’s not met:

from pyalex import Institutions, Institution

result_set = Institutions().search("MIT").get()
item = result_set[0]

assert isinstance(item, Institution)
print(item["display_name"])  # safe after assert

What’s really going on

Pylance and pyright reason about code statically. Without type hints in third-party libraries, they fall back to inference which is necessarily conservative. Missing overloads for methods like Institutions.get exacerbate the issue by describing the return as a broad union rather than the precise shape you actually get in a particular call path. The code executes correctly because Institution is a dict subclass, making string-based indexing valid; the checker, however, cannot prove that from the available information.

Why this matters

Unchecked unions lead to noisy diagnostics and, worse, can hide real issues. Explicitly narrowing types restores signal-to-noise ratio in your editor, keeps refactors safe, and avoids teaching your team to ignore warnings. When working with untyped packages, being intentional about type narrowing is the cleanest way to keep static analysis useful.

Takeaways

When a library is untyped, communicate intent to the type checker. Narrow down unions using cast, isinstance, or assert, depending on how defensive you want to be. If you need to inspect what the checker believes about a symbol, use reveal_type in your IDE to see the inferred type. And whenever you encounter list-like vs dict-like confusion in diagnostics, verify the inferred type first, then narrow it explicitly so your tooling can keep up with your runtime reality.