|
1 | | -PEP: 812 Title: Immutable variables with const keyword Author: Michael Voznesensky <mvoz@google.com> |
| 1 | +PEP: 812 |
| 2 | +Title: Immutable variables with const keyword |
| 3 | +Author: Michael Voznesensky <mvoz@google.com> |
| 4 | +Status: Draft |
2 | 5 | Type: Standards Track |
| 6 | +Content-Type: text/x-rst |
| 7 | +Created: 31-Oct-2025 |
3 | 8 |
|
4 | | -# Abstract |
| 9 | +Abstract |
| 10 | +======== |
5 | 11 |
|
6 | | -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. |
| 12 | +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. |
7 | 13 |
|
8 | | -This PEP proposes a `const` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. |
| 14 | +This PEP proposes a ``const`` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. |
9 | 15 |
|
10 | | -# Proposal |
| 16 | +Proposal |
| 17 | +======== |
11 | 18 |
|
12 | | -A `const` keyword that applies to functions, class attributes, and variables. |
| 19 | +A ``const`` keyword that applies to functions, class attributes, and variables. |
13 | 20 |
|
14 | | -# Motivation |
| 21 | +Motivation |
| 22 | +========== |
15 | 23 |
|
16 | | -To elaborate on the cases above, consider the following code: |
| 24 | +To elaborate on the cases above, consider the following code:: |
| 25 | + |
| 26 | + def add_item_to_cart(item, cart=[]): |
| 27 | + """ |
| 28 | + Adds an item to a user's cart. |
| 29 | + If no cart is provided, starts a new one. |
| 30 | + """ |
| 31 | + cart.append(item) |
| 32 | + return cart |
17 | 33 |
|
18 | | -``` |
19 | | -def add_item_to_cart(item, cart=[]): |
20 | | - """ |
21 | | - Adds an item to a user's cart. |
22 | | - If no cart is provided, starts a new one. |
23 | | - """ |
24 | | - cart.append(item) |
25 | | - return cart |
26 | | -``` |
27 | 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. |
28 | 35 |
|
29 | | -Or |
| 36 | +Or:: |
| 37 | + |
| 38 | + def analyze_latest_scores(current_scores): |
| 39 | + original_order = current_scores |
| 40 | + current_scores.sort(reverse=True) |
| 41 | + return { |
| 42 | + "top_score": current_scores[0], |
| 43 | + "first_entry": original_order[0] |
| 44 | + } |
30 | 45 |
|
31 | | -``` |
32 | | -def analyze_latest_scores(current_scores): |
33 | | - original_order = current_scores |
34 | | - current_scores.sort(reverse=True) |
35 | | - return { |
36 | | - "top_score": current_scores[0], |
37 | | - "first_entry": original_order[0] # Bug: This will be the top score, not the first entry |
38 | | - } |
39 | | -``` |
40 | | -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! |
| 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! |
41 | 47 |
|
42 | 48 | And, aside from these edge cases of mutability, just general readability and safety added to python. |
43 | 49 |
|
44 | | -# What does `const` mean? |
| 50 | +What does ``const`` mean? |
| 51 | +========================= |
| 52 | + |
| 53 | +There are two tiers of ``const``-ness - this proposal pushes for the strictest version of it. |
| 54 | + |
| 55 | +Less restrictive - ``const`` only forbids rebinding |
| 56 | +--------------------------------------------------- |
45 | 57 |
|
46 | | -There are two tiers of `const`-ness - this proposal pushes for the strictest version of it. |
| 58 | +In this variant of ``const``, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages. |
47 | 59 |
|
48 | | -## Less restrictive - `const` only forbids rebinding |
| 60 | +.. code-block:: python |
49 | 61 |
|
50 | | -In this variant of `const`, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages. |
| 62 | + const x = [] |
| 63 | + x = {} # Fails, no rebinding allowed, raises |
| 64 | +
|
| 65 | +However:: |
| 66 | + |
| 67 | + const x = [] |
| 68 | + x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated |
51 | 69 |
|
52 | | -``` |
53 | | -`const` x = [] |
54 | | -x = {} # Fails, no rebinding allowed, raises |
55 | | -``` |
56 | | -However: |
57 | | -``` |
58 | | -`const` x = [] |
59 | | -x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated |
60 | | -``` |
61 | 70 | In this case, theres not much to do with function arguments, except catch shadowing as an exception. |
62 | | -In this case, the mutable default problem presented above is not resolved. |
63 | | - |
64 | | -## More restrictive - `const` forbids direct mutation |
65 | | - |
66 | | -``` |
67 | | -`const` x = [] |
68 | | -x = {} # Fails, no rebinding allowed, raises |
69 | | -``` |
70 | | -And: |
71 | | -``` |
72 | | -`const` x = [] |
73 | | -x.append(1) # Fails, modifying the object declared as `const`, illegal |
74 | | -``` |
75 | | -And |
76 | | -``` |
77 | | -class MyWidget: |
78 | | - x: int |
79 | | - |
80 | | - def update(self, x): |
81 | | - self.x = x |
82 | | - |
83 | | -m = MyWidget() |
84 | | -m.update(1) # 1, sound |
85 | | -m.update(2) # 2, sound |
86 | | -`const` n = MyWidget() |
87 | | -n.update(1) # Fails, updating a `const` |
88 | | -``` |
89 | | -Variables marked as `const` cannot be updated, and raise upon updated |
90 | | - |
91 | | -# Usage |
92 | | - |
93 | | -There are three primary uses of the `const` keyword proposed here: |
94 | | - |
95 | | -- On function arguments |
96 | | -- On attributes and fields classes |
97 | | -- On variables |
98 | | - |
99 | | -## On function arguments |
100 | | - |
101 | | -An argument marked as `const`, be it an arg or a kwarg, functions exactly as if you were to define a local variable at the top of the function as `const`. It cannot be modified, and the object it refers to cannot be updated or written to in any way. It can only be passed to functions that also expect it as "`const`" - that is, you cannot erase `const`ness once it is applied. It can be copied out to a non `const` variable, and that is the proposed analogue of `const`_cast here, the only way to un-`const` something is via a copy. |
| 71 | +In this case, the mutable default problem presented above is not resolved. |
102 | 72 |
|
103 | | -Shadowing a name becomes an exception. |
| 73 | +More restrictive - ``const`` forbids direct mutation |
| 74 | +---------------------------------------------------- |
104 | 75 |
|
105 | | -``` |
106 | | -def foo(`const` bar, baz): |
107 | | - bar = 3 # Fails, raises on shadowing |
108 | | - return bar * baz |
109 | | -``` |
| 76 | +.. code-block:: python |
110 | 77 |
|
111 | | -``` |
112 | | -def boo(bat, bat): |
113 | | - ... |
| 78 | + const x = [] |
| 79 | + x = {} # Fails, no rebinding allowed, raises |
114 | 80 |
|
115 | | -def foo(`const` bar, baz): |
116 | | - boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` |
117 | | - ... |
118 | | -``` |
| 81 | +And:: |
119 | 82 |
|
120 | | -## On attributes and fields in classes |
| 83 | + const x = [] |
| 84 | + x.append(1) # Fails, modifying the object declared as const, illegal |
121 | 85 |
|
122 | | -This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a `const` variable after. |
| 86 | +And:: |
123 | 87 |
|
124 | | -``` |
125 | | -class MyWidget: |
126 | | - `const` x: int |
127 | | - |
128 | | - def update(self, x): |
129 | | - self.x = x # Fails, always raises, x is `const` |
| 88 | + class MyWidget: |
| 89 | + x: int |
130 | 90 |
|
131 | | -``` |
| 91 | + def update(self, x): |
| 92 | + self.x = x |
132 | 93 |
|
133 | | -## On variables |
| 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 |
134 | 99 |
|
135 | | -Mostly covered above, but either a local or global can be declared `const`, and enforces renaming and update semantics described above. |
| 100 | +Variables marked as ``const`` cannot be updated, and raise upon updated |
136 | 101 |
|
137 | | -Can only be passed functions where the argument is marked `const`. |
| 102 | +Usage |
| 103 | +===== |
138 | 104 |
|
139 | | -# Compiler benefits |
| 105 | +There are three primary uses of the ``const`` keyword proposed here: |
140 | 106 |
|
141 | | -## Globals |
| 107 | +* On function arguments |
| 108 | +* On attributes and fields classes |
| 109 | +* On variables |
142 | 110 |
|
143 | | -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 a LOAD_GLOBAL instruction. |
144 | | -``` |
145 | | -DEBUG = False |
146 | | -def foo(): |
147 | | - if DEBUG: |
148 | | - ... |
149 | | - if DEBUG: |
150 | | - ... |
151 | | -``` |
152 | | -Looks like: |
153 | | -``` |
154 | | -Disassembly of <code object foo at 0x561367afd590, file "example.py", line 2>: |
155 | | - 2 RESUME 0 |
| 111 | +On function arguments |
| 112 | +--------------------- |
| 113 | + |
| 114 | +An argument marked as ``const``, be it an arg or a kwarg, functions exactly as if you were to define a local variable at the top of the function as ``const``. It cannot be modified, and the object it refers to cannot be updated or written to in any way. It can only be passed to functions that also expect it as "``const``" - that is, you cannot erase ``const``ness once it is applied. It can be copied out to a non ``const`` variable, and that is the proposed analogue of ``const_cast`` here, the only way to un-``const`` something is via a copy. |
156 | 115 |
|
157 | | - 3 LOAD_GLOBAL 0 (DEBUG) |
158 | | - TO_BOOL |
159 | | - POP_JUMP_IF_FALSE 1 (to L1) |
| 116 | +Shadowing a name becomes an exception. |
160 | 117 |
|
161 | | - 4 NOP |
| 118 | +.. code-block:: python |
162 | 119 |
|
163 | | - 5 L1: LOAD_GLOBAL 0 (DEBUG) |
164 | | - TO_BOOL |
165 | | - POP_JUMP_IF_FALSE 1 (to L2) |
| 120 | + def foo(const bar, baz): |
| 121 | + bar = 3 # Fails, raises on shadowing |
| 122 | + return bar * baz |
166 | 123 |
|
167 | | - 6 RETURN_CONST 0 (None) |
| 124 | +.. code-block:: python |
168 | 125 |
|
169 | | - 5 L2: RETURN_CONST 0 (None) |
170 | | -``` |
171 | | -Today, when, you could store the value once and skip the LOAD_GLOBALS, as well as the control flow (in this case). Further static analysis features could then kick in to mark the dead branch as dead. |
| 126 | + def boo(bat, bat): |
| 127 | + ... |
172 | 128 |
|
173 | | -## Class safety / MRO optimization |
| 129 | + def foo(const bar, baz): |
| 130 | + boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` |
| 131 | + ... |
174 | 132 |
|
175 | | -If a class method is marked const, the compiler knows it will never be overridden by a subclass or shadowed by an instance attribute. |
176 | | -When you call my_obj.const_method(), the compiler doesn't need to check the instance dictionary or walk the MRO. It can compile a direct call to that exact function object. |
| 133 | +On attributes and fields in classes |
| 134 | +----------------------------------- |
177 | 135 |
|
178 | | -## Guarding Jits |
| 136 | +This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a ``const`` variable after. |
179 | 137 |
|
180 | | -JITs that rely on guards (Cpython jit, torchdynamo, etc) could emit less guards |
| 138 | +.. code-block:: python |
181 | 139 |
|
182 | | -# Non compiler benefits |
| 140 | + class MyWidget: |
| 141 | + const x: int |
183 | 142 |
|
184 | | -Cleaner, more readable code. |
| 143 | + def update(self, x): |
| 144 | + self.x = x # Fails, always raises, x is const |
185 | 145 |
|
186 | | -Stronger invariants at a language level. |
| 146 | +On variables |
| 147 | +------------ |
187 | 148 |
|
| 149 | +Mostly covered above, but either a local or global can be declared ``const``, and enforces renaming and update semantics described above. |
188 | 150 |
|
| 151 | +Can only be passed functions where the argument is marked ``const``. |
0 commit comments