Allow abstract classes to be assigned to `type[Proto]`?

The typing spec currently requires that a class object assigned to type[Proto] must be a concrete class, not an abstract class or a protocol class. This is a bit weird, in that it means the type object Proto is not assignable to type[Proto]. The rationale for it is to ensure constructibility of type[Proto], but that rationale seems a bit weak given that all constructibility of type[] types is basically an “if you’re lucky” situation, since constructor signatures are not subject to Liskov.

There has been a lot of demand over the years for this restriction to be removed (see that thread for many real-world use cases), and at least at one point mypy maintainers were leaning toward removing this restriction. But we still find ourselves in a position today where the spec and conformance suite explicitly require this arguable-at-best behavior.

I think this rule is too controversial to be mandated by the spec and conformance suite. Type checkers should be free to experiment here with what rule works best for their users. I propose that we make this restriction explicitly optional in both the spec and conformance suite. This would not require any existing type checker to remove the restriction.

Any objections?

10 Likes

I love your proposal to get rid of the rule that type[X] is constructible.

I think it should go the other way: type[X] should not be callable at all. If you want a callable, you should type it Callable[[], X].

This issue had a lively debate on the subject.

4 Likes

I don’t disagree, but that’s a much larger change than what I’m proposing here, with significant backwards-compatibility concerns. I think it’s something a type checker could experiment with in an opt-in way, and I’d like to do that in ty.

I see the change I’m proposing here as a small complementary step that shouldn’t be too controversial.

5 Likes

I don’t think further weakening what people can expect out of type[T] is likely to be a net positive, but I do understand the appeal in isolation. The existing uses of type[T] are not the same as Callable[..., T], at best, it’s closest to the currently not expressible type[T] & Callable[ConstructionSignatureOf[T], T]: replacement with a non-type callable is not a suitable replacement[1], even if this appears temping due to it allowing (with manual effort, not automatically) expressing the construction signature.

If we were going to weaken type to allow things that can’t even be constructed at runtime from the object, it may instead be a good time to consider a new special form that expresses that it’s any type system type without a restriction on it also being a runtime type.


  1. Plenty of uses involve passing this to things like 3-argument form of type, or to a type registry that inspects attributes (see dataclasses + cattrs) ↩︎

Yes, I agree. Baby steps. It took us six years to get here from that issue :slight_smile:

(Great job on Ty by the way! Love the design choices.)

3 Likes

Maybe make an additional symbol that is a non-callable type in ty_extensions and recommend its use?

If you’d like to discuss the idea of non-callable type[], let’s do that in a different thread. The topic of this thread is just making it optional for type checkers to prevent passing abstract classes to type[Proto].

2 Likes

I’m in favour of the change, but wonder if letting type checkers “free to experiment” would be the best here. As I recall from some of the summaries of the Typing Summit one of the points the community agreed on was that the difference in behaviour (e.g., using different error codes) between type checkers makes it harder to adopt and try out different ones on existing code bases.

The status quo is of course that checkers behave differently but from an end user point of view I’d prefer if we moved towards more consistent behaviour instead of making the typing spec have more optional restrictions.

1 Like

I tried opening a thread in the past concerning this topic, together with a GitHub issue in the typing specification repository. Both of them had not much success (with great frustration I must add, but alas it is what it is). Perhaps the OP of this thread will be luckier than me.

My typical use case is to use type[Proto] to express the possibility of making a guard/filter function that can return an instance of an object if it passes the isinstance check against a reference protocol type. It’s all part of a framework leveraging discoverable plugins, but in general that’s the go-to approach that some of the users in the issue pointed by @NeilGirdhar usually refer to.

And concerning Callable, I’m not particularly convinced and I would probably go for a dedicated Constructable type as pointed by @mikeshardmind (hopefully I didn’t misunderstand their intention). Still that objection was refuted multiple times in different conversations (in the past an AbstractType was suggested).

And I think that the issue shouldn’t be left to type checkers for experimentation but formalized in the typing specification through appropriate PEPs. There have been in the past (hopefully my mind is not playing tricks on me) complaints or points raised about the fact that type checkers are divergent in behaviour and ruleset and there was a desire for more convergence. I get that experimentation allows for discovering possible new use case scenarios involving more complex type annotations, but for issues like these that have been going (on the mypy side) for over 6 years, there should be a desire for a formalization. Or at least a more serious discussion about it.

It’s a specification, if it’s not meant as such shouldn’t it be renamed to guideline then? And this is a little bit of a deliberate provocation, so take it with a grain of salt.

Can we be more precise here and say that type[T] is constructable with param type **P if and only if T(*args, **kwargs) is accepted for *args: P.args, **kwargs: P.kwargs? (Materializing any gradual types along the way.)
That way, cls: type[Proto]; cls() will be rejected, but cls: type[Any]; cls() and cls: type[int]; cls() will still be valid.

2 Likes

Not sure what behavior you have in mind for such a thing, but that description seems confusingly close to TypeForm

I’ve yet to figure out how to use TypeForm with protocols or abstract types in general. Neither the PEP or the typing documentation have been helpful in this matter.

Will that also reject:

class X:
  pass

def f(cls: type[X]):
  cls()

Because subclasses of X may not be callable.

It shouldn’t reject that. That would reject valid code on the basis that the type system doesn’t reject subtypes that break substitution.

The assumption shouldn’t be that most users are breaking LSP. In fact, if someone is actually specifying type[X], then it’s pretty clear that they expect only things that conform to the expectations of type[X]. Assuming pathological use because the type system itself doesn’t check that the use isn’t pathological is hotile to users.

4 Likes

I would support lifting this restriction. type[T] should mean “instances of type that are T or a subclass”. I think that’s the most sensible and simple rule.

Ideally we should eventually disallow calling type[], but that’s a change for another day (probably after intersection types).

7 Likes

@Jelle Would you prefer to leave this up to type checker discretion, as I initially proposed, or to specify the absence of this restriction, as others in the thread have suggested?

Either is fine with me. I’d prefer to specify the absence of the restriction, but if maintainers of type checkers that currently enforce the restriction feel strongly, we can leave it up to discretion.

4 Likes

Whoa whoa whoa friends, this thread is getting dangerously close to being back on-topic!

I am not proposing (and don’t want to propose) any new restrictions on constructibility of type[T] in this thread. I think if we restricted calling type[Proto], that would break existing use cases where something is typed as type[Proto] and then constructed, but in practice all type objects actually passed in are concrete and constructible. I am not aiming to break those use cases. I am just proposing that abstract types also be assignable to type[Proto], to enable other non-construction use cases that people clearly have.

1 Like