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

The type of None is NoneType [not a built-in, it’s type(None)], but in typing we use None as a type:

my_var: None|int = 3

Besides of typing, Pyhon does not accept None as a type. Technically None is indeed not a type. OTOH None is quite special.

I think this difference between usage of None in typing and not-typing should be somehow addressed. I’m just not sure how.

My particular use-case looks like this. I will focus on the isinistance check:

def convert_wrapper(arg, ignore_types):
    # ignored types may contain zero, one or more types
    if isinstance(arg, ignore_types):
        return arg
    return convert(arg)

I want to be able to include None as an ignored type just like it is commonly used in typing.

Using a tuple doesn’t work. As a special case, None occurences must be replaced with type(None):

isinstance(arg, (int, None))          # TypeError in isinstance
isinstance(arg, (int, type(None)))    # OK

Using a union seems to work, but there is again a special case:

isinstance(arg, typing.Union[*(int, None)])   # OK
isinstance(arg, typing.Union[*()]) 
        # TypeError: Cannot take a Union of no types

I’d like to be able to make a test without handling special cases and if possible using the one obvious way. The least modification that would allow to do that is IMO to allow an empty type union - if it does not break anything else, of course.

3 Likes

I think it’s important here to highlight the distinction between types and classes. Classes are Python objects that are used to make other objects, things like int or str. Types are sets of objects that share some common properties and behaviours. Some types like “the set of all integers” or “the set of all strings” just are the sets of objects that are instances of a particular class, so we just use the class’s name to spell those types. So int and str also are types in addition to being classes. But some more complicated types aren’t collections of all instances of one single class. For example, int | str or list[str].

The thing with isinstance is that it conceptually doesn’t do type checking but class instance checking. For simple types like int or str those are the same thing, but not for the more complex types. In principle, the second argument of isinstance must specify a class, not a more general type. This is the reason why you’ll get an error when you call e.g. isinstance([1, 2, 3], list[int]).

But isinstance also has two layers of ergonomics in it: first, people wanted to check whether an object is an instance of not just one specific class but one of several options, so the isinstance(1, (str, int)) overload was introduced. Note that here the second argument again isn’t actually a type, it’s a tuple of classes. Then once we had union syntax people saw that the semantics of asking whether an object is an instance of one of several classes actually is the same as asking whether it is a member of the union of those classes. Because of that, another overload was added and isinstance now also accepts (non-empty) unions.

In my (and several other peoples’) opinion, that last option was a mistake. It muddles the waters by making it seem like isinstance can handle arbitrary types as the second argument and checks type membership. When really it just accepts a selection of classes and checks instance relations. This also is why it doesn’t accept None and only NoneType. While the former is a valid way to express the type that contains the object None, that isn’t what isinstance actually deals with.

In general, you probably won’t get around having to add a bunch of other checks and special cases to functions like the one you’re describing, even if None is special cased by isinstance. Type checking is a fairly involved process and most types can’t just be thrown into isinstance. This one special case might make some code that only cares about very specific types a bit cleaner, but in general it doesn’t really help much and just adds confusion over what exactly a fairly simple built in actually is supposed to do.

14 Likes

You can use types.NoneType.

A

I’ve never really needed to pass None to isinstance. If I’m narrowing down a type that may be None, I pretty much always use the if x is None idiom.

4 Likes

This is the one obvious way.

1 Like

Unfortunately not for the problem I have posted.

I don’t see how this is a special case.
isinstance supporting bare None would be a special case.

1 Like

There is a symmetry between declaration and instance test:

x: int
isinstance(x, int)

x: bool
isinstance(x, bool)

x: str
isinstance(x, str)

# and so on. Except this one:

x: None      # usually in union with other types
# isinstance(x, None)
x is None

If the type to be tested is a parameter, the broken symmetry currently implies there must be some kind special handling.

The special handling comes from being allowed to use None in type annotations as a shorthand for NoneType. It is just a convenience when writing type annotations and does not imply that None can be used in place of NoneType anywhere else.

2 Likes

The way I see it this pretty much boils down to:

“special casing None for isinstance in similar way it is special-cased in typing

Benefit: convenience and consistency with typing
Cost: special casing, risk of more special cases

If there is certainty, that typing is not going to special case anything else in the same manner, then consistent special casing of None in both places might be a good idea. I have been noticing this as something that could be a bit smoother as well, given:

if a is None:       # If only none check needed
isinstance(a, int)  # If only one type needed
isinstance(a, (int, type(None))  # Extra typing needed when both

And from my experience this is quite common pattern.


However, my concern is: What if typing adds another special case? Then another? Is the fact that typing special cased something sufficient reason to trickle that special case down to lower levels of the language?


If things are not 100% clear, I would say keep this as potential, give it time until they are. My first question would be: “What is the probability that typing will special-case something else in similar manner, say in the period of 20/50 years?”

I am slightly positive if there is some sort of guarantee and reasoning that “It will be limited to None”, why it makes sense and how can we be sure.


In short, if there is certain certainty that making things consistent will effortlessly retain the consistency for foreseeable future, then drawing a parallel might be a good idea given the convenience benefit. Otherwise, if there is a risk that parts are still prone to move, drawing a parallel which will potentially be broken might do more harm than benefit.

1 Like

isinstance(x, None) does not work because None is a singleton. It is an instance of isinstance(None, type(None))

None is not really a special case here.

There are lots things you can’t put in isinstance that you can use in a type annotation.

  • isinstance(x, Final)
  • isinstance(x, list[int])
  • isinstance(x, Annotated[int, “something”])
4 Likes

Github search `/\bisinstance\(.*type\(None\).*\)/ ~ 50K files

Does anyone know why the typing developers decided to use None instead of NoneType?

Probably convenience. None is a builtin, NoneType requires an import. And T | None is a very common annotation.

1 Like

The discrepancy between None in typing and elsewhere could be fixed by removing the special casing of None in typing. I could consider it an illustration of the following from the Zen of Python: “Special cases aren’t special enough to break the rules.” One reason is that special-casing tends to breed more requests for special-casing. typing could have defined NoneT = type(None) to eliminate most of the extra typing. Or one can do that at the top of a file instead of importing.

Perhaps None should have been made a class with no instances. I am not sure what would break if we did that now. a is None should continue to work in conditions. To test, someone could change the definition of None and replace NoneType = type(None) with NoneType = None in the types module and run the test suite.

5 Likes

I think this is true for everything but None. None is already very special and for good reasons. So while I’ve personally not needed to do isinstance(obj, None), I wouldn’t be against such a special case. It’s intuitive and non-breaking.

1 Like

Using None as a type more generally would be bad news for code like this:

def f() -> type|None: ...
t = f()
if t is not None:
    ...

How would you tell the difference between None-the-type and None-the-missing-value-placeholder?

Yup, this change is very improbable. The design of None has been taken as an example for other sentinel implementations.

And personally, I like Sentinel-Singleton design and would not like to change it.

And I find no issue with doing isinstance(var, type(Sentinel)).

But this thread, I think, is about whether to make this one exception for None given how fundamental it is in Python, and that exceptions have already been made for it in other places.

I’d like to note a few things.

Indeed, None has been special cased in the context of annotations, as it is very common to have a None-able type.

Without the special case, majority of modules in typed code bases would need to import types or from types import NoneType to be able to correctly annotate the None-able type.


In situations where x is statically known to be None-able, x is None is the idiomatic way.

If there’s a need to test for None as well as a set of other types, one can do if x is None or isinstance(x, <other types>).
This proposal would allow to write it instead as isinstance(x, (None, <other types>)).
Personally, I would not use this form, so it’s a net negative for me.[1]


In the original post, the value is not statically known to be None-able.
I think this situation is even less common.
Since the check is done with isinstance, one has to specifically pass in NoneType. This seems to be the main pain point of this thread, as NoneType is either an import, or a call to type(None).
Perhaps you should be voting instead to make NoneType a global built-in.

Anyhow:
Currently, to correctly specify the parameter type of convert_wrapper, one may simply do type | tuple[type, ...].
With the new feature, unless None became assignable to type, you’d need to expand that to type | tuple[type | None, ...] | None.

None being assignable to type would open another can of worms.

If I were the user of that function, I’d probably double check as to why it accepts None in the same place it asks for class types.


Finally, (pedantic hat) None is not an instance of None. It’s an instance of NoneType/type(None).
Although similar argument could be made for annotations (None is not a type)


  1. the feature ever so slightly slows down isinstance due to the need to check for None ↩︎