Skip to content

mypy cannot resolve Experiment.fetch() — TypedDict from generated_types shadows logger class #498

@willfrey

Description

@willfrey

Summary

braintrust.Experiment resolves to the wrong type under mypy. Both a TypedDict (the REST API response shape, from generated_types) and a class (the ObjectFetcher subclass, from logger) are exported under the same name. At runtime the class wins, but mypy picks the TypedDict — making .fetch(), .log(), etc. invisible to static analysis.

Reproduction

# repro.py
import braintrust

def f(exp: braintrust.Experiment) -> None:
    for row in exp.fetch():  # error: "Experiment" has no attribute "fetch"
        print(row["id"])
$ mypy repro.py
repro.py:4: error: "Experiment" has no attribute "fetch"  [attr-defined]

Importing directly from the submodule works around it:

from braintrust.logger import Experiment  # resolves correctly

Root cause

__init__.py does:

from .generated_types import *  # line 69 — brings TypedDict `Experiment`
from .logger import *           # line 75 — brings class `Experiment`

At runtime the logger class wins (last import). But mypy's wildcard-import resolution is undefined for conflicting names and in practice picks the TypedDict from generated_types.

Affected names

Name generated_types logger
Experiment TypedDict (API response shape) Class (ObjectFetcher)
Dataset TypedDict (API response shape) Class (ObjectFetcher)
Prompt TypedDict (API response shape) Class
Project TypedDict (API response shape) dataclass

Possible resolutions

There are a few ways to fix the ambiguity — depends on what's intended to be public:

  1. Disambiguate the names — rename the generated TypedDicts (e.g. ExperimentResponse, DatasetResponse) so both can coexist at the top level without collision.
  2. Remove TypedDicts from the public namespace — exclude the colliding names from generated_types.__all__ so only the runtime classes export at the top level. TypedDicts remain importable from braintrust._generated_types.
  3. Ensure the class wins in mypy — add explicit from .logger import Experiment as Experiment after the wildcards (note: I tested this and it did NOT resolve the issue in mypy 2.1.0; only removing the TypedDicts from the wildcard did).

I verified locally that option 2 resolves the issue — after removing Experiment, Dataset, Prompt, and Project from generated_types.py's __all__, mypy correctly resolves the logger classes. But option 1 may be preferable if users depend on the TypedDicts at the top level.

Environment

  • braintrust 0.24.0 (latest)
  • mypy 2.1.0
  • Python 3.14

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions