You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today, python variables are wonderfully mutable - this is a super power of the language. However, in larger codebases, or more complex implementations, there is often a need to mark a variable as immutable. This is useful in two major ways, the first of which is general to programming, and not specific to python - assurances of objects staying identical for their lifetimes is a powerful hint to both the programmer, and the compiler. Potential compiler opitmizations aside, ``const`` hints ignal to programmers that the original author of the code intended this object not to change, which, like c++'s ``const`` corectness ideas, promotes safety and readability. Potential general programming benefits aside, a far more specific python "Gotcha", is mutable defaults.
15
+
Today, Python variables are wonderfully mutable - this is a super power of the language. However, in larger codebases, or more complex implementations, there is often a need to mark a variable as immutable. This is useful in two major ways. First, general to programming and not specific to Python, assurances of objects staying identical for their lifetimes is a powerful hint to both the programmer and the compiler. Potential compiler optimizations aside, ``const`` hints signal to programmers that the original author of the code intended this object not to change, which, like C++'s ``const`` correctness ideas, promotes safety and readability.
13
16
14
-
This PEP proposes a ``const`` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable.
15
-
16
-
Proposal
17
-
========
18
-
19
-
A ``const`` keyword that applies to functions, class attributes, and variables.
17
+
Second, a far more specific Python "Gotcha" is mutable defaults. This PEP proposes a ``const`` keyword that can be inserted in function arguments, defaults, class attributes, and scopes to declare an object as immutable, forbidding both rebinding and mutation.
20
18
21
19
Motivation
22
20
==========
23
21
24
-
To elaborate on the cases above, consider the following code::
22
+
Consider the following code::
25
23
26
24
def add_item_to_cart(item, cart=[]):
27
25
"""
@@ -31,9 +29,9 @@ To elaborate on the cases above, consider the following code::
31
29
cart.append(item)
32
30
return cart
33
31
34
-
cart is evaluated *once* when the function is defined - this means that a second caller appending to the cart is going to see the first item, and so forth, - a common mistake.
32
+
In this standard example, ``cart`` is evaluated *once* when the function is defined. This means that a second caller appending to the cart is going to see the first item, and so forth—a common mistake.
35
33
36
-
Or::
34
+
Another example involves accidentally mutating data that should be a snapshot::
37
35
38
36
def analyze_latest_scores(current_scores):
39
37
original_order = current_scores
@@ -43,70 +41,69 @@ Or::
43
41
"first_entry": original_order[0]
44
42
}
45
43
46
-
It looks like we are saving a snapshot of the data as it came in... but .sort() modifies the list *in-place*. Because 'original_order' is just a reference to 'current_scores', the returned "first_entry" field is will be the top score, not the first entry!
44
+
It looks like we are saving a snapshot of the data as it came in, but ``.sort()`` modifies the list *in-place*. Because ``original_order`` is just a reference to ``current_scores``, the returned "first_entry" field will be the top score, not the first entry.
47
45
48
-
And, aside from these edge cases of mutability, just general readability and safety added to python.
46
+
Beyond these edge cases of mutability, a ``const`` keyword adds general readability and safety to Python.
49
47
50
-
What does ``const`` mean?
51
-
=========================
48
+
Rationale
49
+
=========
52
50
53
-
There are two tiers of ``const``-ness - this proposal pushes for the strictest version of it.
51
+
The inclusion of ``const`` provides significant benefits in both tooling and language semantics.
54
52
55
-
Less restrictive - ``const`` only forbids rebinding
In this variant of ``const``, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages.
56
+
Globals Optimization
57
+
''''''''''''''''''''
59
58
60
-
.. code-block:: python
59
+
If the compiler knows a global is ``const``, it can bake its value directly into the bytecode of functions that use it, rather than emitting ``LOAD_GLOBAL`` instructions.
61
60
62
-
const x = []
63
-
x = {} # Fails, no rebinding allowed, raises
61
+
Consider the following standard Python code::
64
62
65
-
However::
63
+
DEBUG = False
64
+
def foo():
65
+
if DEBUG:
66
+
...
67
+
if DEBUG:
68
+
...
66
69
67
-
const x = []
68
-
x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated
70
+
Currently, this results in repeated ``LOAD_GLOBAL`` instructions and runtime checks. With a ``const`` global, the compiler can store the value once, skip the ``LOAD_GLOBAL`` opcodes, and potentially use static analysis to identify and remove the dead branches entirely.
69
71
70
-
In this case, theres not much to do with function arguments, except catch shadowing as an exception.
71
-
In this case, the mutable default problem presented above is not resolved.
72
+
Class Safety and MRO Optimization
73
+
'''''''''''''''''''''''''''''''''
72
74
73
-
More restrictive - ``const`` forbids direct mutation
If a class method is marked ``const``, the compiler guarantees it will never be overridden by a subclass or shadowed by an instance attribute. When calling ``my_obj.const_method()``, the compiler does not need to check the instance dictionary or walk the Method Resolution Order (MRO). It can compile a direct call to that exact function object.
75
76
76
-
.. code-block:: python
77
+
JIT Guard Reduction
78
+
'''''''''''''''''''
77
79
78
-
const x = []
79
-
x = {} # Fails, no rebinding allowed, raises
80
+
JIT compilers that rely on guards (such as CPython's JIT, torchdynamo, etc.) can emit fewer guards, as the invariants provided by ``const`` reduce the number of state changes that need monitoring.
80
81
81
-
And::
82
+
Non-Compiler Benefits
83
+
---------------------
82
84
83
-
const x = []
84
-
x.append(1) # Fails, modifying the object declared as const, illegal
85
+
***Readability**: Code becomes cleaner and easier to reason about.
86
+
***Invariants**: Provides stronger invariants at the language level, reducing classes of bugs related to accidental mutation.
85
87
86
-
And::
88
+
Specification
89
+
=============
87
90
88
-
class MyWidget:
89
-
x: int
91
+
This proposal pushes for the strictest version of ``const``-ness: forbidding both rebinding and direct mutation.
90
92
91
-
def update(self, x):
92
-
self.x = x
93
+
Syntax and Semantics
94
+
--------------------
93
95
94
-
m = MyWidget()
95
-
m.update(1) # 1, sound
96
-
m.update(2) # 2, sound
97
-
const n = MyWidget()
98
-
n.update(1) # Fails, updating a const
96
+
Variables marked as ``const`` cannot be updated, and raise an exception upon update attempts.
99
97
100
-
Variables marked as ``const`` cannot be updated, and raise upon updated
98
+
Rebinding is forbidden::
101
99
102
-
Usage
103
-
=====
100
+
const x = []
101
+
x = {} # Fails, no rebinding allowed, raises
104
102
105
-
There are three primary uses of the ``const`` keyword proposed here:
103
+
Mutation is forbidden::
106
104
107
-
* On function arguments
108
-
* On attributes and fields classes
109
-
* On variables
105
+
const x = []
106
+
x.append(1) # Fails, modifying the object declared as const, illegal
110
107
111
108
Function Arguments
112
109
------------------
@@ -119,21 +116,13 @@ Key behaviors include:
119
116
* **Transitive Constness**: A ``const`` argument can only be passed to other functions that also expect it as ``const``. You cannot erase "constness" once it is applied.
120
117
* **Explicit Copying**: It can be copied out to a non-``const`` variable. This is the proposed analogue to C++'s ``const_cast``; the only way to "un-const" something is via a copy.
121
118
122
-
Reassignment and Shadowing
123
-
^^^^^^^^^^^^^^^^^^^^^^^^^^
124
-
125
-
126
-
Shadowing or reassigning a ``const`` name is treated as an exception.
127
-
128
-
.. code-block:: python
119
+
Shadowing or reassigning a ``const`` name is treated as an exception::
129
120
130
121
def foo(const bar, baz):
131
122
bar = 3 # Fails, raises on reassignment/shadowing
132
123
return bar * baz
133
124
134
-
When passing a ``const`` variable to another function, the receiving function's arguments must also be marked ``const``.
135
-
136
-
.. code-block:: python
125
+
When passing a ``const`` variable to another function, the receiving function's arguments must also be marked ``const``::
137
126
138
127
# Standard function with mutable arguments
139
128
def boo(bat, man):
@@ -147,9 +136,7 @@ When passing a ``const`` variable to another function, the receiving function's
147
136
Class Attributes and Fields
148
137
---------------------------
149
138
150
-
Marking an attribute as ``const`` makes it writable only at ``__init__`` time (or assignable via a default value). It is illegal to modify a ``const`` attribute after initialization.
151
-
152
-
.. code-block:: python
139
+
Marking an attribute as ``const`` makes it writable only at ``__init__`` time (or assignable via a default value). It is illegal to modify a ``const`` attribute after initialization::
153
140
154
141
class MyWidget:
155
142
const x: int
@@ -160,98 +147,56 @@ Marking an attribute as ``const`` makes it writable only at ``__init__`` time (o
160
147
Variables
161
148
---------
162
149
163
-
As covered in previous sections, both local and global variables can be declared ``const``. This enforces the renaming and update semantics described above.
150
+
Both local and global variables can be declared ``const``. This enforces the renaming and update semantics described above. Critically, these variables can only be passed to functions where the corresponding argument is also marked ``const``.
164
151
165
-
Critically, these variables can only be passed to functions where the corresponding argument is also marked ``const``.
152
+
Backwards Compatibility
153
+
=======================
166
154
167
-
Benefits
168
-
========
155
+
This proposal should be generally sound regarding backwards compatibility, except for cases where ``const`` is currently used as a variable name. This will become a ``SyntaxError``, which can be detected statically (via linting) and is relatively trivial to fix in existing codebases.
169
156
170
-
Compiler Benefits
171
-
-----------------
157
+
Security Implications
158
+
=====================
172
159
173
-
Globals Optimization
174
-
^^^^^^^^^^^^^^^^^^^^
160
+
Introducing strict immutability may enhance security by preventing certain classes of injection or state-tampering attacks where mutable shared state is exploited. No negative security implications are immediately foreseen, though the implementation of "frozen" objects must ensure that standard Python sandboxing or restricted execution environments cannot bypass constness.
175
161
176
-
If the compiler knows a global is ``const``, it can bake its value directly into the bytecode of functions that use it, rather than emitting ``LOAD_GLOBAL`` instructions.
177
-
178
-
Consider the following standard Python code:
179
-
180
-
.. code-block:: python
162
+
How to Teach This
163
+
=================
181
164
182
-
DEBUG=False
183
-
deffoo():
184
-
ifDEBUG:
185
-
...
186
-
ifDEBUG:
187
-
...
165
+
[Placeholder: Instructions for teaching this feature to new and experienced Python users.]
188
166
189
-
Currently, this results in repeated ``LOAD_GLOBAL`` instructions and runtime checks:
167
+
Reference Implementation
168
+
========================
190
169
191
-
.. code-block:: text
170
+
The implementation is planned in phases (WIP):
192
171
193
-
Disassembly of <code object foo at 0x561367afd590, file "example.py", line 2>:
194
-
2 RESUME 0
172
+
* **Phase 1 (Rebinding):** Implementing standard "final" behavior. Bytecodes used for assignment (``STORE_FAST``, etc.) will be extended to look up const tagging and fail if necessary.
173
+
* **Phase 2 (Frozen Objects):** New bytecodes that set flags propagating the constness of the object to the underlying implementation. This will start with builtin types (``PyList``, ``PyDict``) and explore intercessions into functions like ``PyList_Append`` to respect the constness of the object.
174
+
* **Phase 3 (Viral Constness):** Implementing the transitive nature of const in function calls.
195
175
196
-
3 LOAD_GLOBAL 0 (DEBUG)
197
-
TO_BOOL
198
-
POP_JUMP_IF_FALSE 1 (to L1)
176
+
Rejected Ideas
177
+
==============
199
178
200
-
4 NOP
179
+
Less restrictive ``const`` (rebinding only)
180
+
-------------------------------------------
201
181
202
-
5 L1: LOAD_GLOBAL 0 (DEBUG)
203
-
TO_BOOL
204
-
POP_JUMP_IF_FALSE 1 (to L2)
182
+
A variant of ``const`` was considered that only forbids rebinding, similar to "final" in Java or ``const`` in JavaScript.
205
183
206
-
6 RETURN_CONST 0 (None)
207
-
208
-
5 L2: RETURN_CONST 0 (None)
209
-
210
-
With a ``const`` global, the compiler can store the value once, skip the ``LOAD_GLOBAL`` opcodes, and potentially use static analysis to identify and remove the dead branches entirely.
211
-
212
-
Class Safety and MRO Optimization
213
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
214
-
If a class method is marked ``const``, the compiler guarantees it will never be overridden by a subclass or shadowed by an instance attribute.
215
-
216
-
When calling ``my_obj.const_method()``, the compiler does not need to check the instance dictionary or walk the Method Resolution Order (MRO). It can compile a direct call to that exact function object.
217
-
218
-
JIT Guard Reduction
219
-
~~~~~~~~~~~~~~~~~~~
220
-
JIT compilers that rely on guards (such as CPython's JIT, torchdynamo, etc.) can emit fewer guards, as the invariants provided by ``const`` reduce the number of state changes that need monitoring.
221
-
222
-
Non-Compiler Benefits
223
-
---------------------
184
+
.. code-block:: python
224
185
225
-
* **Readability**: Code becomes cleaner and easier to reason about.
226
-
* **Invariants**: Provides stronger invariants at the language level, reducing classes of bugs related to accidental mutation.
186
+
const x = []
187
+
x = {} # Fails, no rebinding allowed, raises
188
+
x.append(1) # Allowed, as the name `x` stays the same object
227
189
190
+
This was rejected because it does not resolve the mutable default problem presented in the Motivation, nor does it provide the strong invariants required for the proposed compiler optimizations.
228
191
229
-
Back-compat
192
+
Open Issues
230
193
===========
231
194
232
-
Should be entirely sound - except for cases where someone is using ``const`` as a variable name. This should become a SyntaxError, which should be relatively trivial to fix, and can be detected entirely statically (linting, etc).
233
-
234
-
235
-
Implementation / Open questions
236
-
===============================
237
-
238
-
Note - this section needs further exploration and is a WIP.
239
-
240
-
Basics
241
-
------
242
-
The implementation would require adding const to the python grammar, updating the ast, and all other language level structures that handle keywords.
243
-
244
-
Less restrictive / phase 1
245
-
--------------------------
246
-
The first phase, rebinding, (or, breaking rebinding, aka, the final keyword like work described above) - seems relatively straightforward. Bytecodes used for assignment (store_fast, etc) - would be extended to look up our const tagging and fail according to the descriptions above.
247
-
248
-
249
-
Frozen objects / phase 2
250
-
------------------------
251
-
For the second phase, more akin to a frozen object, we would need to come up with new bytecodes that set flags that propagate the constness of the object to the underlying implementation. I think we would start with builtin types (PyList, PyDict) and start exploring intercessions into functions like PyList_Append to respect the constness of the object.
252
-
195
+
* **Viral Constness Implementation**: strictly enforcing transitive constness may incur a type check on every function call with const keywords in it, which may be prohibitively expensive or complex to implement efficiently.
196
+
* **Phase 2 Scope**: The extent to which "frozen" objects can be implemented across all C-extension types remains an open question.
253
197
254
-
Viral constness / phase 3
255
-
-------------------------
256
-
Viral constness seems tricky to implement, as it would incur a type check on every function call with const keywords in it.
198
+
Copyright
199
+
=========
257
200
201
+
This document is placed in the public domain or under the
202
+
CC0-1.0-Universal license, whichever is more permissive.
0 commit comments