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
Prevents accidental mutations of intended constants
Formalizes existing convention without introducing new keywords
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.
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.
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.
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.
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.
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.?
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.
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
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.
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.)