Quick context, I’m a newbie here. I’m putting together a course on advanced Python (I’m a professor) and digging through various aspects of Python with an eye towards helping students make effective use of Python’s rich set of features.
I was reading about Python built-in constants (Python docs 3.13.1) and saw that two constants (Ellipsis and NotImplemented) can have their values changed at run-time. So I tried assigning to them and promptly was able to break things. For instance, taking an example that seems popular on StackOverflow for Ellipsis and breaking it:
from numpy import arange
a = arange(16).reshape(2, 2, 2, 2)
print(a[Ellipsis, 0].flatten())
Ellipsis = []
print(a[Ellipsis, 0].flatten())
print(a[..., 0].flatten())
The first and third print give the expected result, but the second print does not…
Fiddling with NotImplemented appears to cause methods such add() to return actual values instead of NotImplemented, with harmful results, but I’m still trying to find a terse example.
So I’m curious, is there a good reason why we can assign new values to Ellipsis and NotImplemented rather than prohibiting it, ala True, False and None?
Because they are not constants, but builtin singletons. Other things you can overwrite with harmful sideeffects:
int
type
numpy/np, the symbol in the a modules globals
sys.modules["numpy"], the cached module object itself
__import__, the entry point to the import machinery.
Python is a very dynamic language, and with very little effort you can break just about everything.
None, True, False are probably the three most common constants and code could become very confusing if they get overwritten, so they get a bit of special protection. But otherwise python does not prevent every stupid mistake people can make.
TBH I don’t even understand why None got special protection. True and False I understand as extension of the protection that is awarded to numbers. But most objects can be re-assigned, even the ones you never want to.
It protects against the typo of a = None = 1 instead of a = None == b, but it’s not like the latter is a common pattern.
Nowadays str and int need protection more than None does, because it’s easy to mess up
x = 'a'
match x:
case str:
print(x)
and now you’ve reassigned str and broken all code (that follows within the same function/script) that uses str to convert something to a string.
It is good to tinker, but as a teacher you need to impress on students that some things should not be done, and unlike other languages, there can be less stopping you in Python than in others.
“We are all responsible users here”
On a similar note, students of other languages often complain about our name-mangling for class variables. I wrote a Rosetta Code task on this topic which shows how class variables can be accessed for many languages.
And lastly, use of eval/exec might be an advanced topic. I wrote another Rosetta Code task specifically to encourage the use of eval/exec for those languages that have it. Note the Python code is much smaller than that of other languages that don’t have eval/exec.
We seem to have slightly different views about teaching. Of course we all seek to be responsible users. At the same time (a) we all make mistakes and another post here shows a case where one can break ‘str’ – similarly someone will accidentally assign to any assignable thing and so warnings of the form “be supercareful here” is an instructor’s job (whereas if nothing broke, I could ignore the fact the constants are assignable); and (b) there are ways to avoid the perils, but you need to know the perils exist – so, in this case, using … in all cases and never using Ellipsis is safe and avoids the perils.
There are some singletons that the compiler itself uses to make control flow optimisations when compiling code (think things like optimising out “always true” and “always false” checks in conditionals). These are protected from shadowing because allowing shadowing would prohibit those optimisations (or make them potentially very confusing if they were still allowed). Making the affected constants (None, True, False) hard keywords in the compiler was a Python 3 change: PEP 3100 – Miscellaneous Python 3.0 Plans | peps.python.org
Other builtins don’t need that level of protection, and being able to shadow (or even outright replace) them is sometimes useful (such as customising their behaviour for specialised environments, or intercepting calls to them for testing purposes). The fact that modifying builtins carelessly (such as replacing NotImplemented or BaseException) will break things for an entire process, and careless shadowing can break things for a given module (hence linters often warning about it), is considered a fair trade-off for the extra flexibility that this approach offers users.
This is a general principle of the language design historically referred to as the “consenting adults” rule, but more inclusively referred to as the “responsible users” rule (since Python has many non-adult users):
“don’t do that, then” is an entirely reasonable way of handling situations where doing something unusual results in obscure exceptions
outside security sensitive situations, we shouldn’t invest too much time in trying to anticipate the unusual things that users might attempt to do and deciding which of them we consider legitimate and which of them we think is worthy of an explicit runtime check and customised exception
Ellipsis specifically is a different case again, since its keyword spelling is ... (three consecutive periods) rather than its English name. Rather than making the English name a keyword, Python 3 instead made the symbolic spelling available in all situations (it was restricted to subscripting operations in Python 2), and (as already noted above) the target of that symbolic setting cannot be modified:
>>> import builtins
>>> Ellipsis is ...
True
>>> builtins.Ellipsis is ...
True
>>> Ellipsis = []
>>> Ellipsis is ...
False
>>> builtins.Ellipsis is ...
True
>>> builtins.Ellipsis = Ellipsis
>>> Ellipsis is ...
False
>>> builtins.Ellipsis is ...
False
Ellipsis cannot be made a keyword because of ast.Ellipsis. It is deprecated now, but there are likely uses of that name in third-party code. And there is no much benefit in this, since you always can use ....
NotImplemented is less common name, so it is safer to make it a keyword. It is also more beneficial, because it is more used in the user code, and LOAD_CONST is faster than LOAD_GLOBAL. The compiler can even special case is NotImplemented like it does for is None.