On Friday, September 6, 2019, 5:41:23 PM PDT, Steven D'Aprano <steve@pearwood.info> wrote:

>On Fri, Sep 06, 2019 at 07:44:19PM +0000, Andrew Barnert wrote:

>> > Union, and unions, are currently types:
>>
>> > py> isinstance(Union, type)
>> > True
>> > 
>> > py> isinstance(Union[int, str], type)
>> > True
>>
>> What version of Python are you using here?
>
> Ah, I didn't realise that the behaviour has changed! I was using Python
> 3.5, but I just tried again in 3.8:

I didn't even consider that this might be an old feature that was taken away, rather than a new feature that was added. Oh well, at least it gave me an excuse to configure and build 3.9. :)

> I don't know the reason for this change, but in the absense of a 
> compelling reason, I think it should be reversed.

My thinking is in the opposite direction. I'm assuming Guido or Jukka or whoever wouldn't have made this change without a compelling reason. And if we can't guess that reason, then someone (who cares more about pushing this change than me) has to do the research or ask the right people to find out. Without knowing that, we can't really decide whether it's worth reversing for all types, or just for union types specifically, or whether the proposal needs to be rethought from scratch (or scrapped, if that's not possible), and the conservative default should be the last one.

But hopefully the default won't matter, because someone will care enough to find out the reasons and report them to us. (Or maybe Guido will just see this discussion and have the answers on the top of his head.)

> Steven (me):

>> > But I disagree that int|str is a subclass of int|str|bytes. There's
>> > no subclass relationship between the two: the (int|str).__bases__
>> > won't include (int|str|bytes), 

> Andrew replied:

> > First, `issubclass` is about subtyping, not about declared inheritance.

> That could be debated. help(subclass) says:

> "Return whether 'cls' is a derived from another class or is the same 
> class."

> but of course ABCs and virtual subclassing do exist.
> My position is that 
> in Python, issubclass is about inheritance, but you can fake it if you
> like, in which case "consenting adults" applies.

But it's not just a "consenting adults" feature that users can do whatever they want with, it's a feature that's used prevalently and fundamentally in the standard library—including being the very core of typing.py—and always with the consistent purpose of supporting (a couple of minor variations on the idea of) subtyping. So sure, technically it is inheritance testing with a bolt-on to add whatever you want (and of course historically, that _is_ how it evolved), but in practice, it's subtype testing.

Thanks to the usual practicality-beats-purity design, it isn't actually testing _exactly_ subtype either, of course. After all, while it would be silly to register `int` with `collections.abc.Sequence`, there's nothing actually stopping you from doing so, and that obviously won't actually make `int` a subtype of `Sequence`, but it will fool `issubclass` into believing it is. But, except when you're intentionally breaking things for good or bad reasons, what it tests is much closer to subtype than inheritance. In fact, even when you break things for good reasons, often it's to better match the subtyping expectation, not to violate it. (Consider `Sequence` and `Mapping`, which act as if they were structural subtyping tests like the other collection ABCs, despite the fact that it's actually impossible to distinguish the two types that way so they cheat with a registry. Try doing that with Swift or Go. :)

> This Stackoverflow post:

union types in scala with subtyping: A|B <: A|B|C

> suggests that Scala considers that int|str is *not* a subtype of
> int|str|bytes, but Scala.js considers that it is.

Without reading the whole question carefully, and without refreshing my fuzzy memory of Scala's type system, I think there are two things going on here.

First, the OP in that question seems to be trying to build his own disjunction type that acts like a union in other languages (including, apparently, Scala.js?), and he just didn't know how to code it right.

So, why would anyone do that in the first place? Well, the bigger issue is that Scala's unions aren't quite the same thing we're talking about here in the first place—despite the fact that they're what inspired this whole thread. In most languages, the union of int and str is a special type that's defined by including all int values and all str values (and nothing else), or something similar to that. In Scala, the union of int and str is defined as the least upper bound of int and str on the lattice of all types (which of course provably does include all int values and all str values, because int and str are subtypes). In simple cases this ends up doing the same thing. And when they differ, it's usually that Scala can infer something cool that other languages can't. But I do vaguely remember one case where Scala couldn't infer a type for me and (unlike, say, Haskell or Kotlin—which may fail more often, but can always tell you exactly why they failed and what you need to add to fix it) it couldn't tell that it couldn't infer it, and it went and did something crazy like… an infinitely-long compile that ate up all my disk space or something? I forget.

> But I will point out that the name of the function is *issubclass*, not 
> *issubtype*. 

Sure, and the `class` statement creates a `type` instance. Which are sometimes called `classes` and sometimes `types`. Since 2.3, all classes are types and all types are classes, and the root of the metaclass hierarchy is called `type`, and Python just mixes and matches the two words arbitrarily, and it almost never causes confusion. (Of course in pre-2.3 Python, they were completely unrelated things, and `issubclass` only worked on classes, hence the name.)

If you try and import a meaning from another language, then sometimes it can get confusing. But think about it: in, say, Java, "subclass" explicitly means only a concrete class inheriting implementation from another concrete class; a class implementing an interface is not subclassing. Which means a test for subclassing in the Java sense would be (a) not what Python does, and (b) pointless.