From 8c16d7aeb4cf0c17198e3bee51ba45865dfc5b39 Mon Sep 17 00:00:00 2001 From: Michael Voznesensky Date: Fri, 31 Oct 2025 11:26:43 -0700 Subject: [PATCH 1/5] Create pep-0812.rst Empty pep --- peps/pep-0812.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 peps/pep-0812.rst diff --git a/peps/pep-0812.rst b/peps/pep-0812.rst new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/peps/pep-0812.rst @@ -0,0 +1 @@ + From e4c42a0f9a6c7c1c7d8b60c8b74fe9ca951b33b6 Mon Sep 17 00:00:00 2001 From: Michael Voznesensky Date: Fri, 31 Oct 2025 12:24:38 -0700 Subject: [PATCH 2/5] pep-0812.rst - draft 0 --- peps/pep-0812.rst | 187 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/peps/pep-0812.rst b/peps/pep-0812.rst index 8b137891791..680ffbc596e 100644 --- a/peps/pep-0812.rst +++ b/peps/pep-0812.rst @@ -1 +1,188 @@ +PEP: 812 Title: Immutable variables with const keyword Author: Michael Voznesensky +Type: Standards Track + +# Abstract + +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. + +This PEP proposes a `const` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. + +# Proposal + +A `const` keyword that applies to functions, class attributes, and variables. + +# Motivation + +To elaborate on the cases above, consider the following code: + +``` +def add_item_to_cart(item, cart=[]): + """ + Adds an item to a user's cart. + If no cart is provided, starts a new one. + """ + cart.append(item) + return cart +``` +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. + +Or + +``` +def analyze_latest_scores(current_scores): + original_order = current_scores + current_scores.sort(reverse=True) + return { + "top_score": current_scores[0], + "first_entry": original_order[0] # Bug: This will be the top score, not the first entry + } +``` +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! + +And, aside from these edge cases of mutability, just general readability and safety added to python. + +# What does `const` mean? + +There are two tiers of `const`-ness - this proposal pushes for the strictest version of it. + +## 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. + +``` +`const` x = [] +x = {} # Fails, no rebinding allowed, raises +``` +However: +``` +`const` x = [] +x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated +``` +In this case, theres not much to do with function arguments, except catch shadowing as an exception. +In this case, the mutable default problem presented above is not resolved. + +## More restrictive - `const` forbids direct mutation + +``` +`const` x = [] +x = {} # Fails, no rebinding allowed, raises +``` +And: +``` +`const` x = [] +x.append(1) # Fails, modifying the object declared as `const`, illegal +``` +And +``` +class MyWidget: + x: int + + def update(self, x): + self.x = x + +m = MyWidget() +m.update(1) # 1, sound +m.update(2) # 2, sound +`const` n = MyWidget() +n.update(1) # Fails, updating a `const` +``` +Variables marked as `const` cannot be updated, and raise upon updated + +# Usage + +There are three primary uses of the `const` keyword proposed here: + +- On function arguments +- On attributes and fields classes +- On variables + +## On function arguments + +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. + +Shadowing a name becomes an exception. + +``` +def foo(`const` bar, baz): + bar = 3 # Fails, raises on shadowing + return bar * baz +``` + +``` +def boo(bat, bat): + ... + +def foo(`const` bar, baz): + boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` + ... +``` + +## On attributes and fields in classes + +This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a `const` variable after. + +``` +class MyWidget: + `const` x: int + + def update(self, x): + self.x = x # Fails, always raises, x is `const` + +``` + +## On variables + +Mostly covered above, but either a local or global can be declared `const`, and enforces renaming and update semantics described above. + +Can only be passed functions where the argument is marked `const`. + +# Compiler benefits + +## Globals + +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. +``` +DEBUG = False +def foo(): + if DEBUG: + ... + if DEBUG: + ... +``` +Looks like: +``` +Disassembly of : + 2 RESUME 0 + + 3 LOAD_GLOBAL 0 (DEBUG) + TO_BOOL + POP_JUMP_IF_FALSE 1 (to L1) + + 4 NOP + + 5 L1: LOAD_GLOBAL 0 (DEBUG) + TO_BOOL + POP_JUMP_IF_FALSE 1 (to L2) + + 6 RETURN_CONST 0 (None) + + 5 L2: RETURN_CONST 0 (None) +``` +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. + +## Class safety / MRO optimization + +If a class method is marked const, the compiler knows it will never be overridden by a subclass or shadowed by an instance attribute. +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. + +## Guarding Jits + +JITs that rely on guards (Cpython jit, torchdynamo, etc) could emit less guards + +# Non compiler benefits + +Cleaner, more readable code. + +Stronger invariants at a language level. + From 8cb4dc42e68c136ff21ccef32900bc411371f6c4 Mon Sep 17 00:00:00 2001 From: Michael Voznesensky Date: Fri, 31 Oct 2025 12:27:01 -0700 Subject: [PATCH 3/5] RST --- peps/pep-0812.rst | 243 ++++++++++++++++++++-------------------------- 1 file changed, 103 insertions(+), 140 deletions(-) diff --git a/peps/pep-0812.rst b/peps/pep-0812.rst index 680ffbc596e..25c4b60a3a1 100644 --- a/peps/pep-0812.rst +++ b/peps/pep-0812.rst @@ -1,188 +1,151 @@ -PEP: 812 Title: Immutable variables with const keyword Author: Michael Voznesensky +PEP: 812 +Title: Immutable variables with const keyword +Author: Michael Voznesensky +Status: Draft Type: Standards Track +Content-Type: text/x-rst +Created: 31-Oct-2025 -# Abstract +Abstract +======== -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. +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. -This PEP proposes a `const` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. +This PEP proposes a ``const`` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. -# Proposal +Proposal +======== -A `const` keyword that applies to functions, class attributes, and variables. +A ``const`` keyword that applies to functions, class attributes, and variables. -# Motivation +Motivation +========== -To elaborate on the cases above, consider the following code: +To elaborate on the cases above, consider the following code:: + + def add_item_to_cart(item, cart=[]): + """ + Adds an item to a user's cart. + If no cart is provided, starts a new one. + """ + cart.append(item) + return cart -``` -def add_item_to_cart(item, cart=[]): - """ - Adds an item to a user's cart. - If no cart is provided, starts a new one. - """ - cart.append(item) - return cart -``` 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. -Or +Or:: + + def analyze_latest_scores(current_scores): + original_order = current_scores + current_scores.sort(reverse=True) + return { + "top_score": current_scores[0], + "first_entry": original_order[0] + } -``` -def analyze_latest_scores(current_scores): - original_order = current_scores - current_scores.sort(reverse=True) - return { - "top_score": current_scores[0], - "first_entry": original_order[0] # Bug: This will be the top score, not the first entry - } -``` -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! +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! And, aside from these edge cases of mutability, just general readability and safety added to python. -# What does `const` mean? +What does ``const`` mean? +========================= + +There are two tiers of ``const``-ness - this proposal pushes for the strictest version of it. + +Less restrictive - ``const`` only forbids rebinding +--------------------------------------------------- -There are two tiers of `const`-ness - this proposal pushes for the strictest version of it. +In this variant of ``const``, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages. -## Less restrictive - `const` only forbids rebinding +.. code-block:: python -In this variant of `const`, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages. + const x = [] + x = {} # Fails, no rebinding allowed, raises + +However:: + + const x = [] + x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated -``` -`const` x = [] -x = {} # Fails, no rebinding allowed, raises -``` -However: -``` -`const` x = [] -x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated -``` In this case, theres not much to do with function arguments, except catch shadowing as an exception. -In this case, the mutable default problem presented above is not resolved. - -## More restrictive - `const` forbids direct mutation - -``` -`const` x = [] -x = {} # Fails, no rebinding allowed, raises -``` -And: -``` -`const` x = [] -x.append(1) # Fails, modifying the object declared as `const`, illegal -``` -And -``` -class MyWidget: - x: int - - def update(self, x): - self.x = x - -m = MyWidget() -m.update(1) # 1, sound -m.update(2) # 2, sound -`const` n = MyWidget() -n.update(1) # Fails, updating a `const` -``` -Variables marked as `const` cannot be updated, and raise upon updated - -# Usage - -There are three primary uses of the `const` keyword proposed here: - -- On function arguments -- On attributes and fields classes -- On variables - -## On function arguments - -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. +In this case, the mutable default problem presented above is not resolved. -Shadowing a name becomes an exception. +More restrictive - ``const`` forbids direct mutation +---------------------------------------------------- -``` -def foo(`const` bar, baz): - bar = 3 # Fails, raises on shadowing - return bar * baz -``` +.. code-block:: python -``` -def boo(bat, bat): - ... + const x = [] + x = {} # Fails, no rebinding allowed, raises -def foo(`const` bar, baz): - boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` - ... -``` +And:: -## On attributes and fields in classes + const x = [] + x.append(1) # Fails, modifying the object declared as const, illegal -This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a `const` variable after. +And:: -``` -class MyWidget: - `const` x: int - - def update(self, x): - self.x = x # Fails, always raises, x is `const` + class MyWidget: + x: int -``` + def update(self, x): + self.x = x -## On variables + m = MyWidget() + m.update(1) # 1, sound + m.update(2) # 2, sound + const n = MyWidget() + n.update(1) # Fails, updating a const -Mostly covered above, but either a local or global can be declared `const`, and enforces renaming and update semantics described above. +Variables marked as ``const`` cannot be updated, and raise upon updated -Can only be passed functions where the argument is marked `const`. +Usage +===== -# Compiler benefits +There are three primary uses of the ``const`` keyword proposed here: -## Globals +* On function arguments +* On attributes and fields classes +* On variables -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. -``` -DEBUG = False -def foo(): - if DEBUG: - ... - if DEBUG: - ... -``` -Looks like: -``` -Disassembly of : - 2 RESUME 0 +On function arguments +--------------------- + +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. - 3 LOAD_GLOBAL 0 (DEBUG) - TO_BOOL - POP_JUMP_IF_FALSE 1 (to L1) +Shadowing a name becomes an exception. - 4 NOP +.. code-block:: python - 5 L1: LOAD_GLOBAL 0 (DEBUG) - TO_BOOL - POP_JUMP_IF_FALSE 1 (to L2) + def foo(const bar, baz): + bar = 3 # Fails, raises on shadowing + return bar * baz - 6 RETURN_CONST 0 (None) +.. code-block:: python - 5 L2: RETURN_CONST 0 (None) -``` -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. + def boo(bat, bat): + ... -## Class safety / MRO optimization + def foo(const bar, baz): + boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` + ... -If a class method is marked const, the compiler knows it will never be overridden by a subclass or shadowed by an instance attribute. -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. +On attributes and fields in classes +----------------------------------- -## Guarding Jits +This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a ``const`` variable after. -JITs that rely on guards (Cpython jit, torchdynamo, etc) could emit less guards +.. code-block:: python -# Non compiler benefits + class MyWidget: + const x: int -Cleaner, more readable code. + def update(self, x): + self.x = x # Fails, always raises, x is const -Stronger invariants at a language level. +On variables +------------ +Mostly covered above, but either a local or global can be declared ``const``, and enforces renaming and update semantics described above. +Can only be passed functions where the argument is marked ``const``. From ec3d22ca2531c2dc3d7d19ef049504d63015c076 Mon Sep 17 00:00:00 2001 From: Michael Voznesensky Date: Fri, 31 Oct 2025 12:44:26 -0700 Subject: [PATCH 4/5] Update pep-0812.rst --- peps/pep-0812.rst | 152 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 129 insertions(+), 23 deletions(-) diff --git a/peps/pep-0812.rst b/peps/pep-0812.rst index 25c4b60a3a1..02006a32786 100644 --- a/peps/pep-0812.rst +++ b/peps/pep-0812.rst @@ -108,44 +108,150 @@ There are three primary uses of the ``const`` keyword proposed here: * On attributes and fields classes * On variables -On function arguments ---------------------- +Function Arguments +------------------ + +An argument marked as ``const`` (whether a positional argument or a keyword argument) functions exactly as if a local variable were defined at the top of the function as ``const``. + +Key behaviors include: -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. +* **Immutability**: The variable cannot be modified, and the object it refers to cannot be updated or written to in any way. +* **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. +* **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. -Shadowing a name becomes an exception. +Reassignment and Shadowing +^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +Shadowing or reassigning a ``const`` name is treated as an exception. .. code-block:: python - def foo(const bar, baz): - bar = 3 # Fails, raises on shadowing - return bar * baz + def foo(const bar, baz): + bar = 3 # Fails, raises on reassignment/shadowing + return bar * baz + +When passing a ``const`` variable to another function, the receiving function's arguments must also be marked ``const``. .. code-block:: python - def boo(bat, bat): - ... + # Standard function with mutable arguments + def boo(bat, man): + ... - def foo(const bar, baz): - boo(bar, bar) # Fails, raises on passing bar to boo's bat, which is not `const` - ... + def foo(const bar, baz): + boo(bar, baz) # Fails: raises on passing 'bar' to boo's 'bat', + # because 'bat' is not marked 'const'. + ... -On attributes and fields in classes ------------------------------------ +Class Attributes and Fields +--------------------------- -This makes the attribute only writable at __init__ time - or assignable with a default. It is illegal to modify a ``const`` variable after. +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. .. code-block:: python - class MyWidget: - const x: int + class MyWidget: + const x: int - def update(self, x): - self.x = x # Fails, always raises, x is const + def update(self, x): + self.x = x # Fails: always raises as 'self.x' is const + +Variables +--------- + +As covered in previous sections, 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``. + +Benefits +======== + +Compiler Benefits +----------------- + +Globals Optimization +^^^^^^^^^^^^^^^^^^^^ + +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. + +Consider the following standard Python code: + +.. code-block:: python + + DEBUG = False + def foo(): + if DEBUG: + ... + if DEBUG: + ... + +Currently, this results in repeated ``LOAD_GLOBAL`` instructions and runtime checks: + +.. code-block:: text + + Disassembly of : + 2 RESUME 0 + + 3 LOAD_GLOBAL 0 (DEBUG) + TO_BOOL + POP_JUMP_IF_FALSE 1 (to L1) + + 4 NOP + + 5 L1: LOAD_GLOBAL 0 (DEBUG) + TO_BOOL + POP_JUMP_IF_FALSE 1 (to L2) + + 6 RETURN_CONST 0 (None) + + 5 L2: RETURN_CONST 0 (None) + +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. + +Class Safety and MRO Optimization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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. + +JIT Guard Reduction +~~~~~~~~~~~~~~~~~~~ +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. + +Non-Compiler Benefits +--------------------- + +* **Readability**: Code becomes cleaner and easier to reason about. +* **Invariants**: Provides stronger invariants at the language level, reducing classes of bugs related to accidental mutation. + + +Back-compat +=========== + +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). + + +Implementation / Open questions +=============================== + +Note - this section needs further exploration and is a WIP. + +Basics +------ +The implementation would require adding const to the python grammar, updating the ast, and all other language level structures that handle keywords. + +Less restrictive / phase 1 +-------------------------- +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. + + +Frozen objects / phase 2 +------------------------ +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. -On variables ------------- -Mostly covered above, but either a local or global can be declared ``const``, and enforces renaming and update semantics described above. +Viral constness / phase 3 +------------------------- +Viral constness seems tricky to implement, as it would incur a type check on every function call with const keywords in it. -Can only be passed functions where the argument is marked ``const``. From e46ffcb641efef7aff5d4e17c1a00018be29726f Mon Sep 17 00:00:00 2001 From: Michael Voznesensky Date: Fri, 31 Oct 2025 18:16:21 -0700 Subject: [PATCH 5/5] Found out about PEP 12 --- peps/pep-0812.rst | 223 +++++++++++++++++----------------------------- 1 file changed, 84 insertions(+), 139 deletions(-) diff --git a/peps/pep-0812.rst b/peps/pep-0812.rst index 02006a32786..4556ba7b1b7 100644 --- a/peps/pep-0812.rst +++ b/peps/pep-0812.rst @@ -5,23 +5,21 @@ Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 31-Oct-2025 +Python-Version: 3.15 +Post-History: Pending +Discussions-To: Pending Abstract ======== -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. +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. -This PEP proposes a ``const`` keyword that can be inserted in function arguments, defaults, and in scopes that declares an object as immutable. - -Proposal -======== - -A ``const`` keyword that applies to functions, class attributes, and variables. +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. Motivation ========== -To elaborate on the cases above, consider the following code:: +Consider the following code:: def add_item_to_cart(item, cart=[]): """ @@ -31,9 +29,9 @@ To elaborate on the cases above, consider the following code:: cart.append(item) return cart -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. +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. -Or:: +Another example involves accidentally mutating data that should be a snapshot:: def analyze_latest_scores(current_scores): original_order = current_scores @@ -43,70 +41,69 @@ Or:: "first_entry": original_order[0] } -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! +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. -And, aside from these edge cases of mutability, just general readability and safety added to python. +Beyond these edge cases of mutability, a ``const`` keyword adds general readability and safety to Python. -What does ``const`` mean? -========================= +Rationale +========= -There are two tiers of ``const``-ness - this proposal pushes for the strictest version of it. +The inclusion of ``const`` provides significant benefits in both tooling and language semantics. -Less restrictive - ``const`` only forbids rebinding ---------------------------------------------------- +Compiler Benefits +----------------- -In this variant of ``const``, we limit it to mean rebinding. It is closer spiritually to "final" in certain other languages. +Globals Optimization +'''''''''''''''''''' -.. code-block:: python +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. - const x = [] - x = {} # Fails, no rebinding allowed, raises +Consider the following standard Python code:: -However:: + DEBUG = False + def foo(): + if DEBUG: + ... + if DEBUG: + ... - const x = [] - x.append(1) # Sound, allowed, as the name `x` stays the same type and object, it merely got mutated +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. -In this case, theres not much to do with function arguments, except catch shadowing as an exception. -In this case, the mutable default problem presented above is not resolved. +Class Safety and MRO Optimization +''''''''''''''''''''''''''''''''' -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. -.. code-block:: python +JIT Guard Reduction +''''''''''''''''''' - const x = [] - x = {} # Fails, no rebinding allowed, raises +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. -And:: +Non-Compiler Benefits +--------------------- - const x = [] - x.append(1) # Fails, modifying the object declared as const, illegal +* **Readability**: Code becomes cleaner and easier to reason about. +* **Invariants**: Provides stronger invariants at the language level, reducing classes of bugs related to accidental mutation. -And:: +Specification +============= - class MyWidget: - x: int +This proposal pushes for the strictest version of ``const``-ness: forbidding both rebinding and direct mutation. - def update(self, x): - self.x = x +Syntax and Semantics +-------------------- - m = MyWidget() - m.update(1) # 1, sound - m.update(2) # 2, sound - const n = MyWidget() - n.update(1) # Fails, updating a const +Variables marked as ``const`` cannot be updated, and raise an exception upon update attempts. -Variables marked as ``const`` cannot be updated, and raise upon updated +Rebinding is forbidden:: -Usage -===== + const x = [] + x = {} # Fails, no rebinding allowed, raises -There are three primary uses of the ``const`` keyword proposed here: +Mutation is forbidden:: -* On function arguments -* On attributes and fields classes -* On variables + const x = [] + x.append(1) # Fails, modifying the object declared as const, illegal Function Arguments ------------------ @@ -119,21 +116,13 @@ Key behaviors include: * **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. * **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. -Reassignment and Shadowing -^^^^^^^^^^^^^^^^^^^^^^^^^^ - - -Shadowing or reassigning a ``const`` name is treated as an exception. - -.. code-block:: python +Shadowing or reassigning a ``const`` name is treated as an exception:: def foo(const bar, baz): bar = 3 # Fails, raises on reassignment/shadowing return bar * baz -When passing a ``const`` variable to another function, the receiving function's arguments must also be marked ``const``. - -.. code-block:: python +When passing a ``const`` variable to another function, the receiving function's arguments must also be marked ``const``:: # Standard function with mutable arguments def boo(bat, man): @@ -147,9 +136,7 @@ When passing a ``const`` variable to another function, the receiving function's Class Attributes and Fields --------------------------- -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. - -.. code-block:: python +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:: class MyWidget: const x: int @@ -160,98 +147,56 @@ Marking an attribute as ``const`` makes it writable only at ``__init__`` time (o Variables --------- -As covered in previous sections, both local and global variables can be declared ``const``. This enforces the renaming and update semantics described above. +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``. -Critically, these variables can only be passed to functions where the corresponding argument is also marked ``const``. +Backwards Compatibility +======================= -Benefits -======== +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. -Compiler Benefits ------------------ +Security Implications +===================== -Globals Optimization -^^^^^^^^^^^^^^^^^^^^ +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. -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. - -Consider the following standard Python code: - -.. code-block:: python +How to Teach This +================= - DEBUG = False - def foo(): - if DEBUG: - ... - if DEBUG: - ... +[Placeholder: Instructions for teaching this feature to new and experienced Python users.] -Currently, this results in repeated ``LOAD_GLOBAL`` instructions and runtime checks: +Reference Implementation +======================== -.. code-block:: text +The implementation is planned in phases (WIP): - Disassembly of : - 2 RESUME 0 +* **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. +* **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. +* **Phase 3 (Viral Constness):** Implementing the transitive nature of const in function calls. - 3 LOAD_GLOBAL 0 (DEBUG) - TO_BOOL - POP_JUMP_IF_FALSE 1 (to L1) +Rejected Ideas +============== - 4 NOP +Less restrictive ``const`` (rebinding only) +------------------------------------------- - 5 L1: LOAD_GLOBAL 0 (DEBUG) - TO_BOOL - POP_JUMP_IF_FALSE 1 (to L2) +A variant of ``const`` was considered that only forbids rebinding, similar to "final" in Java or ``const`` in JavaScript. - 6 RETURN_CONST 0 (None) - - 5 L2: RETURN_CONST 0 (None) - -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. - -Class Safety and MRO Optimization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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. - -JIT Guard Reduction -~~~~~~~~~~~~~~~~~~~ -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. - -Non-Compiler Benefits ---------------------- +.. code-block:: python -* **Readability**: Code becomes cleaner and easier to reason about. -* **Invariants**: Provides stronger invariants at the language level, reducing classes of bugs related to accidental mutation. + const x = [] + x = {} # Fails, no rebinding allowed, raises + x.append(1) # Allowed, as the name `x` stays the same object +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. -Back-compat +Open Issues =========== -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). - - -Implementation / Open questions -=============================== - -Note - this section needs further exploration and is a WIP. - -Basics ------- -The implementation would require adding const to the python grammar, updating the ast, and all other language level structures that handle keywords. - -Less restrictive / phase 1 --------------------------- -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. - - -Frozen objects / phase 2 ------------------------- -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. - +* **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. +* **Phase 2 Scope**: The extent to which "frozen" objects can be implemented across all C-extension types remains an open question. -Viral constness / phase 3 -------------------------- -Viral constness seems tricky to implement, as it would incur a type check on every function call with const keywords in it. +Copyright +========= +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive.