Skip to content

Overlapping type #2154

Description

@srittau

This is an rough idea from the recent discussion of whether to use object/Any or _KT for arguments of certain methods like pop or __sub__. E.g.:

class MyPoppable[KT, VT]:
    def pop(self, x: X) -> VT: ...

What should X be? Using KT (or KT | None) finds type errors like the following:

poppable: MyPoppable[str]
poppable.pop(123)

But has a problem with overlapping types:

def foo(x: int | str):
    poppable.pop(x)

On the other hand, using object doesn't catch type errors like poppable.pop(123).

The solution would be able to specify that any partially overlapping type would be accepted:

class MyPoppable[KT, VT]:
    def pop(self, x: Overlapping[KT]) -> VT: ...

MyPoppable[str]().pop() would accept str, str | None, Literal["x"], str | int, etc., but not e.g. plain int.

Activity

  1. added
    topic: featureDiscussions about new features for Python's type annotations
    on Jan 19, 2026
  2. JelleZijlstra commented on Jan 22, 2026

    @JelleZijlstra
    Member

    I've also thought about this but I think it can't be made sound. Consider:

    y: int = 1
    x: Overlapping[str] = y  # error
    
    y: int = 1
    tmp: object = y  # ok
    x: Overlapping[str] = tmp  # ok
    

    @randolf-scholz's suggestion that intersection types would help here seems incorrect. In the example, we have a method pop with parameter type T & str. Whatever T is, T & str is a subtype of str, and int | str is not a subtype of str, so pop(x) where x: int | str should not be allowed.

  3. srittau commented on Jan 22, 2026

    @srittau
    CollaboratorAuthor

    Such an overlapping type would equal object in the implementation, and would only be checked in input locations. This is actually consistent with the problem it's supposed to solve.

  4. AhamadAlii commented on Jan 28, 2026

    @AhamadAlii

    I agree with the concern about soundness, but I think the practical problem is still real and currently unsolved in a usable way.

    What people want to express with Overlapping[KT] is not a real runtime type, but an argument constraint: “accept anything that might be a KT at runtime”. That is inherently input-only and contravariant, which already puts it outside what Python’s nominal type system can model soundly.

    From that perspective, treating it as object at runtime (as srittau suggests) but checking overlap only at call sites is actually consistent with how we already accept other unsound-but-useful typing features (e.g. Any, bivariance of mutable containers, most overloads in typeshed).

    The int | str case is exactly the pain point:

    rejecting it is technically sound but annoying in practice

    accepting it is unsound but matches runtime behavior and user expectations for APIs like dict.pop, get, contains, etc.

    I don’t think intersection types alone solve this, but a checker-only “overlap constraint” (not a real type) might still be worth standardizing, even if it’s explicitly documented as unsound. Otherwise, we’re stuck with object + overload hacks forever.

  5. JelleZijlstra commented on Mar 5, 2026

    @JelleZijlstra
    Member

    I just added a prototype of this feature to pycroscope. It was quite simple to implement: JelleZijlstra/pycroscope#291.

    Still a bit concerned about how this fits in with the set-theoretic formalization of the type system, but it seems like a useful and simple feature.

    $ pycroscope tmp/overlap.py 
    
    Incompatible argument type for x: expected Overlapping[int] but got str (code: incompatible_argument)
      str is not assignable to Overlapping[int]
    
    In tmp/overlap.py at line 7
       4:     pass
       5: 
       6: def caller(s: str, i_s: int | str, i: int):
       7:     f(s)
                ^
       8:     f(i_s)
       9:     f(i)
      10: 
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: featureDiscussions about new features for Python's type annotations

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions