Proposal: Make UPPER_CASE variables automatically immutable

Problem

Python currently has a strong convention that variables in UPPER_CASE should be treated as constants, but there’s no enforcement. This can lead to accidental modifications.

Proposed Solution

Make any variable whose name consists entirely of uppercase characters automatically immutable after its first assignment. Any attempt to reassign would raise a ConstantError.

Example

MAX_SIZE = 100
MAX_SIZE = 200  # This would raise ConstantError

Benefits

  1. Prevents accidental mutations of intended constants

  2. Formalizes existing convention without introducing new keywords

  3. Backward-compatible for code that follows the convention correctly

Technical Considerations

  • Would work for module-level, class-level, and instance variables

  • Could be implemented at compiler/bytecode level

Philosophy Alignment

While Python follows “we’re all consenting adults”, this change would help prevent simple bugs while respecting existing conventions.

You should have a look at PEP 726 – Module __setattr__ and __delattr__ which was a more generic solution to such problem. Sadly, the PEP got rejected.

3 Likes

Thanks Victor for the reference! I see PEP 726 was more about generic module attribute control.

My proposal is different because:

  1. It’s much narrower - only applies to UPPER_CASE variables
  2. It’s convention-based rather than syntax-based
  3. Could be implemented at compiler level rather than module level
  4. Aligns with existing community practices

The rejection of PEP 726 was about its complexity, but this is a simpler, more focused approach.

Is there anything in this proposal which couldn’t be implemented as a python linter / code quality checker (ala flake8, etc?)

From my perspective the proposal definitely breaks perfectly valid existing Python code today (I definitely change global constants in test code to validate behavior). I have also implemented CLI flags which set/change global all-caps constants.

Because the proposal makes existing code invalid it needs to be opt in and building it as a lint / analysis check gives that knob. If it gets really wide adoption then can evaluate bringing into language core.

Another option might also be possible to implement this sort of behavior using a descriptor to make things “only gettable” after first set.

8 Likes

This is not a good idea as the convention that uppercase is a constant is quite loose. It’s not uncommon for code to use such variables for global config values that are “rarely modified” in user code, but may still be changed in testing etc. Preventing reassignment of such names would break existing code.

10 Likes

I have sympathy, but this just can’t work. “Backward compatible” is a very high bar, and anything based on “backward-compatible for code that follows the convention correctly” is dead on arrival.

Code can be complicated indeed. For example, I have a number of “sophisticated” programs that rely heavily on multiprocessing. They routinely access module-level globals, whose names are in all caps. But they’re typically rebound by a multiprocessing.Pool worker initialization function [1], which is passed specific values taken from the command line. There are ways I could restructure all that stuff to work with your vision, but, as-is, they’d just break. Multiply by, at least, many thousands of others, and the howls would be deafening.

OTOH … yes, this kind of code can get so complicated that unintentional rebinding can also be a source of subtle bugs. It’s happened rarely to me, but I once spent hours in all baffled by why N didn’t have the value I was certain it “must” have. N happened to be rebound by some one-shot “what if?” debugging code I slammed in, which I forgot to delete.

Your idea would have spared me that. OTOH, it would also have blocked me from adding the 'one-shot debugging code" to begin with.

So, sorry, I just don’t see a simple way to get there from where we are.


  1. not using fork, but spawn ↩︎

4 Likes

Changing this for all modules is a major breaking change. Making this optional – possible.

But you do not need to wait for a new Python for such feature. You can already enable it yourself. Implement the types.ModuleType subclass that oveerides __setattr__() and __delattr__(), than patch the class of the specific module:

import sys, types
class MyModuleType(types.ModuleType):
    def __setattr__(self, name, value):
        if name.isupper():
            raise AttributeError
        return super().__setattr__(name, value)
    def __delattr__(self, name):
        if name.isupper():
            raise AttributeError
        super().__delattr__(name)

sys.modules[__name__].__class__ = MyModuleType

You can define this class in a separate module and use it in several other modules.

4 Likes

Yes, but not always though.
Uppercases are sometimes used for modifiable global variables.
And even in case of constant, they are sometimes modified as part of definition, e.g.:

CONSTANT = 2
if something:
    CONSTANT *= 2
if something_else:
    CONSTANT += 2

Of course, this can be done without re-definition, but as Python currently allows this and it is sometimes more convenient to do it this way, this would break a lot of things.

Yeah, IMO, this nuance largely falls more under umbrella of “we’re all consenting adults”.

If someone has issues with this, the best long term solution is to develop context aware constant naming strategy, which would not only solve this issue, but also make the module more easily digestible.

I think optional linter rule would be enough here.

This does seem to only work when re-defining constant from outside and is not applicable when constant is re-defined inside the module as per OPs example.?

2 Likes

Occasionally I use a capital letter for naming a set, because that’s the convention in mathematics (and it’s useful).

So I would hate this.

4 Likes

Unmentioned so far is the other sense of immutability. Should this be allowed?

NAMES = ["PcMant"]
NAMES.append("nedbat")
3 Likes

Not only that, I think dicts might be used even more often.

ATTRIBUTES= {
    "name": "...",
    "min_supported_version": 3.6
}

if sys.version_info > (3, 10):
    ATTRIBUTES["min_supported_version"] = 3.10
3 Likes

Good catch! Me too. I’d forgotten that. I also use, e.g., things like L, M, R in bisection-like algorithms (left, middle, right), because the lowercase letters (especially 'l`) can be hard to make out unambiguously in many fonts.

5 Likes

is Final not a readily available solution?

from typing import Final

MAX_SIZE: Final = 100
MAX_SIZE = 200
4 Likes

Yeah, but that would only work during type checking time.

If it is really, really important, and the user needs an error to be raised, sys.settrace (iirc, perhaps the name was different), can be used.

1 Like

Why not frozen dataclasses?

or

class _Const:
    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise TypeError(f"Can't rebind constant '{name}'")
        self.__dict__[name] = value

const = _Const()
const.PI = 3.14159
4 Likes

I think using the name of a variable, type, function, or any object is a bad way to change the semantics of the variable on the language level. Having some dedicated syntax could work, although I think that if immutability is really what you want you should use annotations and a type checker since the project is likely to be fairly complex at that point, or use a less dynamic language than Python.

1 Like

Is that even actually true? Or is it rather the other way around, and constants are just the most common case and the only one you noticed?

PEP 8 for example has a section Constants which says:

Constants are usually defined on a module level and written in all capital letters with underscores separating words.

That’s “if constant, then uppercase”. Not “if uppercase, then constant”. (And I don’t see that elsewhere, either, at least not explicitly.)

Constants are not the only valid reason to use uppercase, there are others. Like the math convention I mentioned, or Tim’s readability (L is even suggested by PEP 8 for readability), or when context like a task specification uses uppercase names (in which case I usually use the same uppercase names in implementing code as well, as I value specification and implementation matching). It’s not ok to break code for using such other valid reasons. (And even if it doesn’t break because it happens to not reassign, making “if uppercase, then constant” a language rule would be misleading, since such code’s reason for uppercase isn’t constantness.)

6 Likes