Best way to emulate type hint intersection of generics

I want to type hint that a function returns the intersection of two types, so my type checker understands that I have access to all elements of both types and doesn’t error.

The best way I found is:

from types import SimpleNamespace

# Use SimpleNamespace to use Any instead of erroring on unknown attribute access
class Base(SimpleNamespace):
    ...

class A(Base):
    a: int

class B(Base):
    b: str

# We could also just use one T here, type checkers will infer T is A | B
def intersect[T1: Base, T2: Base](type1: type[T1], type2: type[T2]) -> type[T1 | T2]:
    ...

intersection = intersect(A, B)

# intersection.a is seen as int | Any
# intersection.b is seen as str | Any

Is there a better solution that doesn’t require inheriting from SimpleNamespace or even removes the Any?

The important bit here is that the classes are not pre-defined, so I cannot just subclass them in code. Type checkers do barely (Pyright) or not all (mypy) understand dynamic type creation with type. Pyright can understand direct usage of type, but using it with generic functions is not understood by it.

I know I could use Any instead of SimpleNamespace as a base class, but that has the issue that anything done with it is not type checked, so SimpleNamespace is better.

Edit: change T1 | T2 to type[T1 | T2] and so on

Protocols? That still requires inheritance though.

There is no way to dynamically generate intersection. You will need to prescribe all combinations.

I know and I layed that out in my post already.

I was asking for the best way to emulate it, because there clearly are ways, like the one I showed…

This gets you the hinting part of intersection as in the editor knows what it can suggest for you but obviously no type safety as all accesses are inplictly uniones with Any which means all guarantees are off.

As for getting anything better I’ve tried to but never come up with anythin workable. (Mostly for the case where I want a decorator to return a class being the intersection of the base class and some Protocol it implemented for that type)

The mechanism for TypeIs already has to support some form of intersection do be useful and and you can actually get pyright to report that but I still haven’t found a way to (ab)use it for any real cases.

1 Like

Well inspired to make a new try I got furthee than I have before.

This is somewhat promising but has a couple of drawbacks such a a only working with inferred types and it would surprise me if more than pyright supported it in this exact form but it sure looks like an intersection to me :joy:

Code sample in pyright playground

import typing as t

class A(t.Protocol):
    a: str

class B(t.Protocol):
    b: int

def supports_t[T](o: object, type: type[T]) -> t.TypeIs[T]:
    return True

def intersect[Ta, Tb](o: object):
    assert supports_t(o, A)
    assert supports_t(o, B)
    return o


ab = intersect(object())

t.reveal_type(ab)
# Type of "ab" is "<subclass of A and B>"

t.reveal_type(ab.a)
# Type of "ab.a" is "str"

t.reveal_type(ab.b)
# Type of "ab.b" is "int"


1 Like

I don’t think that example quite works as is since intersect does not use [Ta, Tb]. A and B are also hardcoded in intersect. However, your example got me interested and I think if we just want a hint we can actually make it work in both pyright and mypy.

from typing_extensions import TypeIs

class A:
    a: str

class B:
    b: int

def supports[T](o: object, type: type[T]) -> TypeIs[T]:
    return True

o = object()
assert supports(o, A) and supports(o, B) # Always succeeds at runtime

reveal_type(o.a) # str
reveal_type(o.b) # int

or slightly less typing for more types

...
class Intersection(A, B, C, D):
    ...

assert supports(o, Intersection)

finally, pyright can even handle

assert supports(o, type('_', (A,B), {}))

Interestingly, mypy stops detecting any errors or print the output for reveal_type after that line.

1 Like

That was just an omission in the short example. It would have to take the types as arguments, alternatively be made a class to allow subscripting at call site to work (hardcoded A and B was just from when I started to see if I could get it to work at all).

This does seem to work in theory as I would want for getting a decorator typed but not sure how stable it would be as Intersection is still not officially supported (even if that is exactly what this is and I’ve even seen pylance report it as A & B).

1 Like

This works perfectly for me! Where my knowledge stopped is that I thought TypeGuard and TypeIs functions could only have one argument, the object to check. I didn’t know they would accept this!

Tysm!

Like mentioned this is relying on things not really specified (that TypeIs applications are implemented as intersections internally and are actually preserved as inferred return values).

I’m glad it works but would be a bit hesitant to rely on it for type hinting reaching public api:s.

I would definetely say be prepared for it to break and/or not work in more complicated cases.

1 Like