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 LiteralString → str 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" |
error — tuple[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")] |
error — list[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.
Summary
An empty container literal (
[]or{}) inside a returned tuple is inferred bottom-up aslist[Never]/dict[Never, Never]and then compared invariantly against the declared element type, producing a falsereturns_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
The
LiteralStringin the message is a red herringThe 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:def f() -> tuple[list[int], str]: return [], "lit"def f() -> tuple[list[int], str]:x: list[int] = []return x, "lit"def f() -> tuple[list[int], str]:s: str = "lit"return [], sSo
LiteralString→stris 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:
def f() -> list[dict[str, object]]: return []x: tuple[list[int], str] = ([], "a")def g(p: tuple[list[int], str]) -> None: ...g(([], "a"))def f() -> tuple[list[int], str]: return ([], "a")def f() -> tuple[list[int], str]: return [], "a"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.def f() -> tuple[dict[str, int], str]: return {}, "a"tuple[dict[Never, Never], LiteralString]def f() -> tuple[set[int], str]: return set(), "a"def f() -> tuple[list[int], list[int]]: return [], []def f() -> list[tuple[list[int], str]]: return [([], "a")]list[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_compatibilityispep-tagged, so it runs undercheckand cannot be downgraded via[tool.basilisk.rules]. It accounts for 6 of 38 false positives in our repo.Real instance:
Environment
basilisk 0.0.0-PLACEHOLDERbuilt frommain(typeshed work), Ruff formatter 0.15.17, macOS arm64typeshed-commit = "35c51b4295956da3fe0d13d4184c6e973f8b2f3a",python-version = "3.12"All checked. No issues found.on every case above. This is a regression on the unreleased branch.