GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Distribute return type over union · python/typing · Discussion #2047

Distribute return type over union #2047

Unanswered
randolf-scholz asked this question in Q&A
Distribute return type over union #2047
@randolf-scholz randolf-scholz
Jul 16, 2025 · 1 comment
Return to top
Discussion options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

randolf-scholz
Jul 16, 2025

Inspired by microsoft/pyright#10673, I looked at this simple example: Code sample in pyright playground

from collections.abc import Sequence

def listify[T](x: Sequence[T]) -> list[T]:
    return list(x)

def func(x: Sequence[str] | Sequence[int]):
    y = listify(x)
    reveal_type(y)  # list[str | int]
    print(y)

It feels like the revealed type is not actually what we want here. The desirable inference is imo list[str] | list[int].

Is there simple way to annotate listify to make the return type distribute over unions, or is this a general type-checker limitation?

What seems to happen here is that we assign Sequence[str] | Sequence[int] <: Sequence[str | int] = Sequence[T], so we pick T = int | str. However, in principle wouldn't it be more precise if the type checker, when seeing a UnionType as an argument, passes every member separately and yields the UnionType of the individual return types?

I guess with multiple arguments this runs into combinatoric explosion...

You must be logged in to vote
All reactions

Replies: 1 comment

Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

ryux1
Sep 7, 2026

Your intuition is supported by the current overload-call algorithm, but the distribution is specific to overload resolution. An ordinary generic call solves T against the argument as a whole, so here it can solve T = str | int and the result is consequently list[str | int].

For this finite set of cases, overloads express the correlation:

from collections.abc import Sequence
from typing import overload

@overload
def listify(x: Sequence[str], /) -> list[str]: ...
@overload
def listify(x: Sequence[int], /) -> list[int]: ...

def listify[T](x: Sequence[T], /) -> list[T]:
    return list(x)

With x: Sequence[str] | Sequence[int], neither overload initially accepts the union as a whole. The typing spec's overload algorithm then performs argument type expansion, checks each union member separately, and unions the successful return types. The result should therefore be list[str] | list[int].

There isn't currently a general annotation that says “map this type constructor distributively over every member of an arbitrary union”. If listify also needs to be publicly generic for types beyond these finite cases, adding an unrestricted generic overload as a fallback defeats this particular trick: that overload can accept the original union directly and infer T = str | int. In that case you need either explicit overloads for the domain where the distinction matters, a separate unrestricted generic function, or narrowing at the call site.

And yes, the potential combinatorial growth is handled explicitly in the overload algorithm: expandable arguments are considered one at a time, and their successful return types are combined.

You must be logged in to vote
All reactions
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
2 participants
@randolf-scholz @ryux1
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.