Constancy in Python Constants

musings by Bast on Thursday October 3nd, 2024

I got asked earlier today "what are all the constants in python." Now, this might have a straightforwards answer (https://docs.python.org/3/library/constants.html), along with a mention of the SCREAMING_SNAKE_CASE convention, but hey.

I got inspired.

So, what makes a constant a constant? What does the word constant mean? If I keep constantly writing constant, does constant lose it's constancy?

A Taxonomy of Constants

The most constant constants are defined by the python syntax. They're also ultimately provided by the C implementation, initialized on interpreter start and cleared at shutdown. They always exist, never change, and raise syntactical errors if you attempt to misuse them:

>>> True = False
  File "<stdin>", line 1
    True = False
    ^
SyntaxError: cannot assign to True

These include True, False, , and None.

Now the curious reader may notice this does not match the list of "constants" as defined by the above reference. Those constants are

  • True
  • False
  • Ellipsis
  • None
  • NotImplemented
  • __debug__

But you can assign to some of these, and not to others. Does this make them more or less of constants? Well, you can absolutely argue that if you can assign to something and change it's meaning:

>>> Ellipsis = 1
>>> Ellipsis
1

It is "less constant" than something you cannot do so to. You can also use Ellipsis in function arguments, etc. The (closest) analogy in python for this behavior is a "Soft keyword". Soft keywords are keywords that are not reserved words, so they can be used like variables, but when use in other expressions have syntactical meaning. Examples of this are the match and case keywords, which construct a match statement, yet can be used like normal variables all the same.

True and False were made non-assignable keywords in python 3, as part of the transition starting in python 2.3 that resulted in the introduction of the boolean type. As a compatibility compromise, assignment was left possible so that old code could continue to use True = 1 and False = 0 to provide boolean compatibility with older python versions.

Interestingly, also, you cannot erase True/False like you can the others either:

>>> Ellipsis
Ellipsis
>>> __builtins__["Ellipsis"] = 1
>>> Ellipsis
1
>>> __builtins__["False"] = 1
>>> False
False
>>> del __builtins__["False"]
>>> False
False

While built-in constants are nominally provided via the __builtins__ module/namespace, it's apparent from behavior that some constants are more constant than others.

If you look for where these special constanter constants come from in the python source, you'll find them in object.c, where they are constructed statically. Except.. so is NotImplemented. And we can assign to that.

On top of that, recent optimizations have been involved in removing reference counts and other behavior from these objects for speed reasons (see https://peps.python.org/pep-0683/). But the list of immortal constants doesn't align with this constancy check–0 and 1 are on the list of immortal constants, but aren't in this list of object.c defined constants or the builtin constants page.

So what we can say is that what actually makes a constantest constant is it's inclusion in the syntactical parse, and where you get a syntax rejection upon misuse (on parse, not execute).

This helps distinguish constant builtin constants from immutable types, which you could also consider constants (more on this in a bit):

>>> True = 1
  File "<stdin>", line 1
    True = 1
    ^
SyntaxError: cannot assign to True
>>> 1 = 1
  File "<stdin>", line 1
    1 = 1
    ^
SyntaxError: cannot assign to literal

Are literals constant? This is a very good question, and without a clear answer. They aren't mentioned in the language constants documentation (but then again, that doesn't mention the differing syntactical restrictions.) But if you look closely you will find that there are literals that are constants, and not just in the straightforwards manner:

struct _Py_static_objects {
    struct {
        /* Small integers are preallocated in this array so that they
         * can be shared.
         * The integers that are preallocated are those in the range
         * -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (exclusive).
         */
        PyLongObject small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];

        PyBytesObject bytes_empty;
        struct {
            PyBytesObject ob;
            char eos;
        } bytes_characters[256];

        struct _Py_global_strings strings;

        _PyGC_Head_UNUSED _tuple_empty_gc_not_used;
        PyTupleObject tuple_empty;

        _PyGC_Head_UNUSED _hamt_bitmap_node_empty_gc_not_used;
        PyHamtNode_Bitmap hamt_bitmap_node_empty;
        _PyContextTokenMissing context_token_missing;
    } singletons;
};

This is a structure of precached constants maintained by the interpreter and built/cleared before/after shutdown. This is where the 0 and 1 constants come from. In current python, this includes integers from -5 to 256, as well as single-character bytestrings, the empty bytestring, the empty tuple, as you can see above. The global strings set includes a large list of all commonly used strings, such as "<dictcomp>" or "list index out of range", which is honestly fascinating.

Note that this list is absolutely an implementation detail, unlike the behaviors of Ellipsis and True and False, which are language properties (and thus we can leverage that to label True and False and Ellipsis "more constant" than 0.

This is further reflected in the Py_GetConstant table initializer:

static PyObject* constants[] = {
    &_Py_NoneStruct,                   // Py_CONSTANT_NONE
    (PyObject*)(&_Py_FalseStruct),     // Py_CONSTANT_FALSE
    (PyObject*)(&_Py_TrueStruct),      // Py_CONSTANT_TRUE
    &_Py_EllipsisObject,               // Py_CONSTANT_ELLIPSIS
    &_Py_NotImplementedStruct,         // Py_CONSTANT_NOT_IMPLEMENTED
    NULL,  // Py_CONSTANT_ZERO
    NULL,  // Py_CONSTANT_ONE
    NULL,  // Py_CONSTANT_EMPTY_STR
    NULL,  // Py_CONSTANT_EMPTY_BYTES
    NULL,  // Py_CONSTANT_EMPTY_TUPLE
};

As we can see, None, True, False, Ellipsis, and NotImplemented exist before the constant table is generated. In fact, they are all compile-time constants, computed in the static C time, whereas the constants for zero, one, empty bytes and empty string are computed during python's startup. While the actual timing for construction of these (and thus they're constantity) depends on C compiler guarantees (which is a rabbit hole we'd best avoid), it's clear that the static definition happens before program start (IE: before Cpython's main() call) and the actual initialization of the empty string happens later (IE: during CPython's main() call).

Thus we can construct six classes of python constants (assuming I didn't miss any, which I probably did! Python is a big language.):

  1. Syntactical constants: True, False, None
  2. Pre-main constants: Ellipsis, NotImplemented
  3. Built-in constants: 0, 1, "", b"", ()
  4. Implementation constants (Static objects): -5 to 256, single-character bytestrings, builtin strings
  5. Interpreter pre-set constants: __debug__
  6. Literals: 1024, etc.
Interestingly, () can be assigned to:
>>> () = []
>>>

It's a no-op, but hey.

and finally

  1. User constants: SCREAMING_SNAKE_CASE

Which are variables in all but their clothing.

( PS: Since "Magic numbers" or "Magic constant" is an entirely semantic distinction, not a syntactical distinction, and therefore the magicity of a constant cannot be evaluated by computers, I didn't consider them. )