Skip to content

Empty container literal inside a returned tuple ignores the declared return type — tuple[list[Never], ...] false positive #337

Description

@MelbourneDeveloper

Summary

An empty container literal ([] or {}) inside a returned tuple is inferred bottom-up as list[Never] / dict[Never, Never] and then compared invariantly against the declared element type, producing a false returns_compatibility. The declared return type is not propagated into the tuple's element positions.

Every element is assignable individually. Only the combination fails.

Minimal repro

def f() -> tuple[list[int], str]:
    return [], "lit"
error[returns_compatibility]: return type mismatch: tuple[list[Never], LiteralString] is not assignable to tuple[list[int], str]
  --> a.py:1:5
error[returns_compatibility_2]: Function `f` return type mismatch: tuple[list[Never], LiteralString] is not assignable to tuple[list[int], str]
  --> a.py:2:5

The LiteralString in the message is a red herring

The message names two suspect types but only list[Never] is actually the problem. Replacing the empty list with an already-typed one clears it while the string literal stays put:

Code Result
def f() -> tuple[list[int], str]: return [], "lit" error
def f() -> tuple[list[int], str]:
x: list[int] = []
return x, "lit"
OK
def f() -> tuple[list[int], str]:
s: str = "lit"
return [], s
error

So LiteralStringstr is fine; list[Never]list[int] is not.

Bidirectional inference works everywhere except this position

The same empty literal is handled correctly at the top level of a return, in annotated assignment, and in a call argument — it is only the nested-in-a-returned-tuple case that fails:

Position Result
def f() -> list[dict[str, object]]: return [] OK
x: tuple[list[int], str] = ([], "a") OK
def g(p: tuple[list[int], str]) -> None: ...
g(([], "a"))
OK
def f() -> tuple[list[int], str]: return ([], "a") error
def f() -> tuple[list[int], str]: return [], "a" error

Parenthesised and bare tuple forms behave identically, so this is not a parsing issue.

Scope

Affects [] and {}; nests to arbitrary depth. set() is a call rather than a display and is unaffected.

Variant Result
def f() -> tuple[dict[str, int], str]: return {}, "a" errortuple[dict[Never, Never], LiteralString]
def f() -> tuple[set[int], str]: return set(), "a" OK
def f() -> tuple[list[int], list[int]]: return [], [] error
def f() -> list[tuple[list[int], str]]: return [([], "a")] errorlist[tuple[list[Never], LiteralString]]

Expected

return [], "lit" against -> tuple[list[int], str] should type-check. An empty container display in a return position should be inferred against the corresponding element of the declared return type, exactly as it already is when it is the entire return value.

Impact

Returning [], None / [], "message" as a (result, error) pair is an extremely common Python idiom, and it is unsuppressable: returns_compatibility is pep-tagged, so it runs under check and cannot be downgraded via [tool.basilisk.rules]. It accounts for 6 of 38 false positives in our repo.

Real instance:

# scripts/gcp_cost_report.py:107
def _bq_json(args: list[str]) -> tuple[list[dict[str, object]], str | None]:
    if shutil.which("bq") is None:
        return [], "bq CLI not found on PATH"

Environment

  • basilisk 0.0.0-PLACEHOLDER built from main (typeshed work), Ruff formatter 0.15.17, macOS arm64
  • typeshed-commit = "35c51b4295956da3fe0d13d4184c6e973f8b2f3a", python-version = "3.12"
  • Not present in released 0.35.0 — 0.35.0 reports All checked. No issues found. on every case above. This is a regression on the unreleased branch.

Metadata

Metadata

Assignees

Labels

criticalCritical severity — fix immediately

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions