Should it be possible to use None as a type (instead of NoneType)?

Is it necessary or advisable to facilitate the replacement of T | None with T | NoneType in the language, e.g. in typing?

In response to the OP’s question, could this be a solution?

>>> NoneType = type(None)
>>> def my_func(...) -> T | NoneType:
    ....

I’ve been following the thread and trying to decide whether or not to suggest this. I don’t think we should, and I’ll share why.

It is genuinely awkward to work with None/NoneType in some cases. I wish there were a nice, harmonious solution for those corner cases, but I’m not sure if there really is a good option (in which case, status quo wins).

NoneType as a global begets the next special case, which is isinstance(x, str | None).

The contract around isinstance() has gotten muddied now that it accepts a UnionType. I’m not exactly complaining. Allowing that had its own rationale. But I think it has negative consequences in terms of keeping ideas clear WRT types vs classes.

My take is that special casing None is at least no worse than allowing unions. If you think allowing unions was a mistake, that points against this.

Putting NoneType into our hands more easily doesn’t really solve the discrepancies, but nor does adding handling for None specifically. Plus, even if it did, you still have user defined enums and sentinels to think about. (Even more so if PEP 661 goes forward.) Maybe the instancehook should be something you can define on class instances, not just classes…? That feels weird but it would only be for special cases.

2 Likes

What’s the upside of doing that? I’ve never seen that someone was actually confused by an annotation like int | None, even though it technically isn’t well formed to have a value there, no one actually parses it that way mentally. We can also see this with other languages having similar forms. Typescript even lets you write e.g. 1 | 2 | 3, which also is super clear that it must not refer to the actual values 1, 2, 3 but to the type containing them.

Everyone who is deep enough into the type annotations rabbit hole that they need to parse them programatically or similar will be well aware of what a None in an annotation means and how to properly handle it. There also is a utility function in the typing module (I can’t recall the exact name right now) that normalizes annotations by collapsing unions of literals, unstringyfying them, etc. It also already swaps out None for NoneType. If we want to make it easier to interact with type annotations at runtime making those utility functions more visible and ergonomic would do so much more than making NoneType a built in and using it in annotations directly.

2 Likes

I’m aware that None is a special value, not a type. My questions were aimed at the OP’s original points, to draw further discussion.

I guess the op should have done that in the first place, as you said, typing._type_convert is fairly well known, although it is private.

The OP would only need to do

import typing

def convert_wrapper(arg, ignore_types):
    # ignored types may contain zero, one or more types
    if isinstance(arg, typing._type_convert(t) for t in ignore_types): # type: ignore # Private Attribute 
        return arg
    return convert(arg)

Maybe however we could “expose” _type_convert to the external typing API.

Note that cyclical types are already a thing in Python:

>>> type(type)
<class 'type'>

As far as I can tell, this is theoretically sound.

Also note that there can only ever be one None, and we can write that as Literal[None]. But usually, we write None instead. Is that wrong? No, because it’s obvious whether something is a type or value by looking at the context. Nothing from type-theory prohibits context-dependent syntax. So writing None as type is perfectly fine.
Now, replace the Literal in the paragraph above with type, and you’ll see that writing None for type[None] is also perfectly fine.

If we make None a type of itself will at runtime, just like how type is it’s own type, the runtime behavior will be more in line with the static None type notation. That sounds like a very good idea to me.

1 Like

I guess type is the type of type cause it itself is a type, like int, str and co are. It’s because all types should be usable as such, being subclasses of type, with some useful attributes. Basically everything defined with class <name>: ... or type(<name>) is of type type unless they have some special metaclass (which itself will be of type type).

Instances on the other side are always objects, so in x = my_class() x will be of type my_class.

None however is an instance of NoneType, so type(None) == NoneType. I think the representation of types in the type system should be evaluatable for some assert cases. assert type(None) == None would however not work, unless we make None an instance of itself. That however would be confusing imo. type is not an instance of type in the classical sense either, it’s just constructed with class type: ..., so None would be a new case, where something is an instance of itself, without having some special casing in the system itself.

Yes, but like I said, this is not the case for None, because the type of None is a unit type, i.e. a type with exactly one inhabitant:

>>> type(None)() is None
True

There is no need to manually construct None like this, but None is already globally accessible. So from a practical point of view, having type(None) be a separate special class serves no purpose. And if something exists that serves no purpose, is is better to get rid of it. We also don’t have a TypeType, TypeTypeType, etc. for that very reason. So why should we have a NoneType if it cannot be used for anything? If the answer is purely based on theory, then I’d recommend you read my previous comment, because is shows that the theoretical framewoirk offers room for having type(None) is None.

Hence the proposal to change that, i.e. so that type(None) is None and isinstance(None, None).

2 Likes

Making None a type means it would acquire a big pile of attributes and
methods that have no business being there. This is completely unlike
type, where they are very much expected to be there.

Seems to me it’s your justification for making None a type that’s purely
theoretical. I can see zero usefulness in this being true at run time.

2 Likes

If None were its own type, we could write

isinstance(x, (int, str, None))

So such a situation would have value.
I just don’t see a way to make it happen with a compelling backwards compatibility story.

1 Like

I don’t think we should be making changes to the runtime like this. type being its own type is special, being the root. The singleton objects are also special as true singletons, but equally all have their own types accessible in the interpreter, and also via the types module (since Python 3.10). The example you gave can be written today using NoneType or type(None), either of which are a better conceptual fit for isinstance.

However, None representing NoneType is a useful thing in the type annotations DSL, even if a slight edge case compared to the runtime.

A

3 Likes

My point isn’t that it’s more useful this way, it’s that it gets rid of the useless NoneType. And as far as I’m concerned, if you can do the same with less, it’s better.

I cannot imagine that someone at Python will modify anything as fundamental as the None. What is new to me is that isinstance was labeled as somehow “muddy” in the discussion. Under these circumstances, the only place where a change maybe could happen is in the isinstance. When we use the None as a type in isinstance, it could be automatically translated to the correct NoneType

How to find out if it is a good idea? For me It boils down to whether the convention introduced in typing is considered a great general idea to adopt or if it is more a typing specific shortcut that we don’t want to proliferate. Hopefully someone can answer that.

When checking if x is None, use x is None.

For type unions, what is wrong with the following:

types = int | str | None
x: types = None
assert isinstance(x, types)

It’s more confusing for users though, for nearly all values (e.g. 1) we can have a type(value) (e.g. int). Same applies to None with NoneType, but here too, it’s not the same thing. type itself already is a special case in many ways, I don’t really see the usage of None being a type. None however is a Literal (an instance), like True and False are. If one wanted to make None a type, they may follow soon. These literals can be checked against with x is y, and there is no reason for isinstance(x, None) to work. Apart from it not being backwards compatible, and x is None being the established way to check for it, isinstance(x, None) seems confusing, as isinstance(x, y) requires type(x) == y.

Generally speaking, I’d be confusing, and there are other, more broadly established ways to check for it.

Also perhaps check out the assure keyword

1 Like