Skip to content

type(None).__module__ returns wrong module #128197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rafalkrupinski opened this issue Dec 23, 2024 · 20 comments
Closed

type(None).__module__ returns wrong module #128197

rafalkrupinski opened this issue Dec 23, 2024 · 20 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@rafalkrupinski
Copy link

rafalkrupinski commented Dec 23, 2024

Bug report

Bug description:

t=type(None)
print(f'{t.__module__}.{t.__name__}')
# prints builtins.NoneType
# actual module: types

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

Linux

@rafalkrupinski rafalkrupinski added the type-bug An unexpected behavior, bug, or error label Dec 23, 2024
@Eclips4
Copy link
Member

Eclips4 commented Dec 23, 2024

Hello!
It's not a bug. None is defined as builtins attribute in the builtins module:

SETBUILTIN("None", Py_None);

@Eclips4 Eclips4 closed this as not planned Won't fix, can't repro, duplicate, stale Dec 23, 2024
@Eclips4 Eclips4 removed the type-bug An unexpected behavior, bug, or error label Dec 23, 2024
@rafalkrupinski
Copy link
Author

rafalkrupinski commented Dec 23, 2024

@Eclips4 It's tot about None, but NoneType

@Eclips4
Copy link
Member

Eclips4 commented Dec 23, 2024

@Eclips4 It's tot about None, but NoneType

Oh, yes, I've misread your message. What made you think that NoneType was coming from types module?

@rafalkrupinski
Copy link
Author

WDYM

>>> from builtins import NoneType
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    from builtins import NoneType
ImportError: cannot import name 'NoneType' from 'builtins' (unknown location)

>>> from types import NoneType
>>> print(NoneType)
<class 'NoneType'>

@Eclips4
Copy link
Member

Eclips4 commented Dec 23, 2024

WDYM

>>> from builtins import NoneType
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    from builtins import NoneType
ImportError: cannot import name 'NoneType' from 'builtins' (unknown location)

>>> from types import NoneType
>>> print(NoneType)
<class 'NoneType'>

That's doesnt mean that NoneType is coming from types. In fact, NoneType is defined in Objects/object.c:

cpython/Objects/object.c

Lines 2119 to 2158 in d61542b

PyTypeObject _PyNone_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"NoneType",
0,
0,
none_dealloc, /*tp_dealloc*/
0, /*tp_vectorcall_offset*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_as_async*/
none_repr, /*tp_repr*/
&none_as_number, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
(hashfunc)none_hash,/*tp_hash */
0, /*tp_call */
0, /*tp_str */
0, /*tp_getattro */
0, /*tp_setattro */
0, /*tp_as_buffer */
Py_TPFLAGS_DEFAULT, /*tp_flags */
none_doc, /*tp_doc */
0, /*tp_traverse */
0, /*tp_clear */
_Py_BaseObject_RichCompare, /*tp_richcompare */
0, /*tp_weaklistoffset */
0, /*tp_iter */
0, /*tp_iternext */
0, /*tp_methods */
0, /*tp_members */
0, /*tp_getset */
0, /*tp_base */
0, /*tp_dict */
0, /*tp_descr_get */
0, /*tp_descr_set */
0, /*tp_dictoffset */
0, /*tp_init */
0, /*tp_alloc */
none_new, /*tp_new */
};

Why do you see builtins in the __module__ of a NoneType? Well, because NoneType is a special object and doesn't actually have a module, so interpreter is doing some hacky stuff:

mod = &_Py_ID(builtins);

This is a long-standing behavior and I don't think we should change it.

@rafalkrupinski
Copy link
Author

NoneType was introduced in 3.10, so not that long, and #63637 says it was in types. How about adding an alias for compatibility?

@Eclips4
Copy link
Member

Eclips4 commented Dec 23, 2024

NoneType was introduced in 3.10, so not that long, and #63637 says it was in types. How about adding an alias for compatibility?

NoneType as type of None wasn't introduced in 3.10, it was introduced much earlier. I don't understand what alias could be added here.

@rafalkrupinski rafalkrupinski changed the title type(None) returns invalid type type(None).__module__ returns wrong module Jan 20, 2025
@rafalkrupinski
Copy link
Author

rafalkrupinski commented Jan 20, 2025

@Eclips4 I'm getting a feeling you're being dismissive towards me.

Why do you see builtins in the module of a NoneType? Well, because NoneType is a special object and doesn't actually have a module, so interpreter is doing some hacky stuff:

I don't have a problem with builtins in __module__, only with NoneType missing from builtins. it's just not there, from python perspective. importing NoneType from builtins raises ImportError!

@Eclips4
Copy link
Member

Eclips4 commented Jan 20, 2025

@Eclips4 I'm getting a feeling you're being dismissive towards me.

Sorry, I just didn't understand you previously. I didn't mean to be dismissive or offend you.

I now understand your confusion and summarize our conversation for others:
type(None).__module__ returns "builtins" and in fact if we would try to import type(None).__name__ from "builtins" it would fail with ImportError. As possible solution we could change it module to types where in fact we have a NoneType.
The same applies for type(some_function), type(some_lambda) and many others.

@Eclips4 Eclips4 reopened this Jan 20, 2025
@Eclips4 Eclips4 added the type-bug An unexpected behavior, bug, or error label Jan 20, 2025
@picnixz picnixz added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Jan 20, 2025
@picnixz
Copy link
Member

picnixz commented Jan 20, 2025

Changing it to types may break existing introspection tools that do special-casing (I think we do something for that in Sphinx). While there is a discrepency, I don't think it's good to change it to types (unless we have guarantees that this won't break things).

Concerning the other builtins, their actual class name is usually different from the exposed name in types, so it wouldn't help to change their module to types (for instance, type(...).__name__ is 'ellipsis' but it's exposed as types.EllipsisType). So we would need to sync the class names with those in types and I don't know if it can break existing code or not (even if such code is bad, it's not good to break stuff if we can avoid it). Surpsingly, type(None) was correctly named (namely types.NoneType exists)

@rafalkrupinski
Copy link
Author

rafalkrupinski commented Jan 21, 2025

Would moving it to types and keeping a deprecated alias in builtins solve the problem for everone?

this would work

from types import NoneType
from builtins import NoneType

this would break

assert type(None).__module__ == 'builtins'

@picnixz
Copy link
Member

picnixz commented Jan 21, 2025

I don't think so because some code might categorize builtins using __module__ == 'builtins'. Strictly speaking, it is correct to assume that it's in the builtins module. It's just that it's not exposed from builtins (which is kinda a "special" module). The same could be said for ... or the type of lambda functions.

By the way, I'd like to know the use case of having type(None).__module__ == 'types' instead of type(None).__module__ == 'builtins'? If it's only for the sake of purity, I would invoke the "practicality beats purity" argument here because it would be easier to keep special casing IMO. Also, it's good to know what is really a built-in or something that was exposed as an opaque type (again, the fact the built-in type is named differently from the exposed name is an indication that such object should not be retrievable from builtins; for None I think it's somehow a coincidence that we have the same name; it could have been none_type, and we would have exposed it as types.NoneType instead).

cc @JelleZijlstra for another opinion (maybe)

@picnixz
Copy link
Member

picnixz commented Jan 21, 2025

Would moving it to types and keeping a deprecated alias in builtins solve the problem for everone?

Also, I think we should rather deprecate the types and move it to builtins. However, builtins is actually more used for anything that can be used without import and NoneType wouldn't be the case =/

OTOH, types.NoneType is just the type of None, but the place where it's been declared is not in types itself (it's like an imported object).

@rafalkrupinski
Copy link
Author

rafalkrupinski commented Jan 21, 2025

I don't mind which module it is in, as long it's consistent. It would simplify all code that relies on it.

My use case is a code generator. For simple cases I can do TypeModel.from(str) or TypeModel.from(list[str]) to create an object that outputs the input type, while other cases work with custom generated classes. Nothing that can't be worked around, but apparently I'm not the only one stumbling upon it.

@rafalkrupinski
Copy link
Author

@picnixz

Strictly speaking, it is correct to assume that it's in the builtins module. It's just that it's not exposed from builtins

Can you elaborate?

For us humans anything goes, but when any metaprogramming or introspection is involved, it's just another special case to handle. I don't see any practical about it.

@picnixz
Copy link
Member

picnixz commented Jan 21, 2025

All objects have a type but some types are special and not exposed as "global" names. The builtins module contains names that are available globally without the needs to import them (and they are also used if you shadow some of the built-in names).

Now, yes, it's special-casing, but this has been a longstanding behaviour. We do not (likely) want to expose globally the type of None, namely we don't want to be able to write something like x = NoneType() without explicitly importing types.NoneType (I don't have access to 3.14 now but we can write type(None)() on 3.11 so I guess it stayed the same). Same holds for function() as function is the type of lambda: None. So we cannot really expose it at runtime to builtins (otherwise globals would be polluted).

While it's a special case, I'm not sure that changing it would work. Unless we expose them in builtins for consistency, I don't think we'll ever change this. I'd say special types require special treatments as well and implementation details were likely meant to be hidden.

Concerning the introspection tools, I think they should deal with it. At least, in Sphinx, we do (https://github.com/sphinx-doc/sphinx/blob/e18155fa106cf64d7b39dcc6506b60fd82703b10/sphinx/util/typing.py#L41) but this is not really hard to maintain. We know this discrepency but I think the standard library can put the burden of this on the tools.

I'm actually unusure of whether the interpreter would still work properly if we were to actually change the module name of built-in types such as functions or type(None). For that, we need some other experts (maybe @brettcannon or @ncoghlan may have a say here?)

@rafalkrupinski
Copy link
Author

OK, bringing those types to builtins might not be the best idea, and come to think about it, it wouldn't help any cases.

I agree it's not a huge problem, and even if it's awkward, it doesn't hit a large part of the user base.

I can't imagine reasonable code that would rely on type(None).__module__ == 'builtins'. For example the sphinx code would keep working with __module__ of these types fixed. And python has seen bigger changes in minor versions.

Then again, I've been surprised before, and I know not all code is reasonable.

@brettcannon
Copy link
Member

There are a bunch of baseline types whose names are not directly exposed in the builtins module:

cpython/Lib/types.py

Lines 13 to 16 in 29caec6

FunctionType = type(_f)
LambdaType = type(lambda: None) # Same as FunctionType
CodeType = type(_f.__code__)
MappingProxyType = type(type.__dict__)

cpython/Lib/types.py

Lines 327 to 332 in 29caec6

GenericAlias = type(list[int])
UnionType = type(int | str)
EllipsisType = type(Ellipsis)
NoneType = type(None)
NotImplementedType = type(NotImplemented)

All of those types list "builtins" as the module because they are "builtins" as a concept and needed some module name as the __module__ attribute is on everything. But a key thing to know is the builtins module only came into existence in Python 3.0, so it isn't really a reference to the module as much as letting the user know the type is built into the language.

And adding anything to builtins is a HUGE deal as it's exposed to all Python code. So expanding builtins just for consistency isn't worth it in my opinion. And considering this is the first time I can remember bringing this up I don't think it's worth changing knowing it will break someone's code who special-cases "builtins" for these exact objects (there's no way there isn't some code out there that relies on this).

As such, I'm closing the issue as "won't fix".

@brettcannon brettcannon closed this as not planned Won't fix, can't repro, duplicate, stale Jan 21, 2025
@JelleZijlstra
Copy link
Member

I do actually think it's worth improving the situation here. If the __module__ attribute does not match the runtime location of the type, that means tools that introspect types at runtime need special casing. I'd rather move to a world where there are fewer such subtleties for introspection.

It's true that existing tools may need updates if we change e.g. NoneType.__module__ to types. However, it's better in the long run to get this right and make it so users don't need to worry about the historical background.

@AlexWaygood
Copy link
Member

I do actually think it's worth improving the situation here. If the __module__ attribute does not match the runtime location of the type, that means tools that introspect types at runtime need special casing. I'd rather move to a world where there are fewer such subtleties for introspection.

I agree, but there's an existing issue for discussing this: #100129

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

6 participants