# Adding Deep Immutability

**URL:** <https://discuss.python.org/t/adding-deep-immutability/92011>\
**Category:** Ideas\
**Created:** [May 14, 2025, 9:27pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011 "2025-05-14T21:27:34Z")\
**Posts on this page:** 20\
**Page:** 2

<div class="post-metadata">

**Author:** ![xFrednet](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/xfrednet/32/27840_2.png) [@xFrednet](https://discuss.python.org/u/xFrednet)\
**Post date:** [May 15, 2025, 11:27pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/21 "2025-05-15T23:27:05Z")

</div>

> [@blhsing](#):
>
> Nice approach to making most objects in Python immutable. One of the important properties of immutable objects to me though is hashability, and I don’t see any mention of it in the PEP or the reference implementation.
> 
> It should be feasible to make all deep-frozen objects hashable since all the primitives are hashable.

That’s an interesting point; thank you for raising it! We haven’t discussed this hashability in detail yet.

My understanding(Please correct me if it’s wrong) is that Python currently uses the presence of the ` __eq__ ()` and ` __hash__ ()` methods to determine if something is hashable. These functions require the object to be immutable, as the hash has to remain constant over the lifetime of the object. Marking currently hashable objects as immutable objects should just work^™️.

However, marking mutable objects hashable might be more complicated. The naive approach would be to add a ` __eq__ ()` or ` __hash__ ()` function to the type on freezing. But this would be unsound since it would mark other (potentially mutable) objects of the same type as hashable as well.

A less clean solution would be to use a check `isfrozen(self)` in ` __hash__ ()` to dynamically restrict the hash implementation to immutable objects. Or maybe have something like ` __frozen_hash__ ()`?

It feels like there is definitely some design space here. I wouldn’t pick it up in this PEP since we haven’t explored the design space and it’s not necessary for the feature to be useful. But this could be something to follow up on when this is implemented

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [May 15, 2025, 11:41pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/22 "2025-05-15T23:41:09Z")

</div>

> [@xFrednet](#):
>
> My understanding(Please correct me if it’s wrong) is that Python currently uses the presence of the ` __eq__ ()` and ` __hash__ ()` methods to determine if something is hashable. These functions require the object to be immutable, as the hash has to remain constant over the lifetime of the object.

No. ` __eq__ ` and ` __hash__ ` need to be consistent with each other, i.e. `a == b` implies `hash(a) == hash(b)` (just not necessarily the reverse) and `hash(a)` is not allowed to change during it’s lifetime; That doesn’t mean that `a` isn’t allowed to change - any such change just can’t be reflected in the hash. E.g. all objects that don’t overwrite ` __eq__ ` are hashable by default by just using their `id`.

---

<div class="post-metadata">

**Author:** ![xFrednet](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/xfrednet/32/27840_2.png) [@xFrednet](https://discuss.python.org/u/xFrednet)\
**Post date:** [May 15, 2025, 11:47pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/23 "2025-05-15T23:47:26Z")

</div>

Ahh, thank you for clarifying!

As said and now proven, we haven’t explored the implication and maybe new possibilities for hashing yet 🙂

---

<div class="post-metadata">

**Author:** ![Eneg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/eneg/32/18177_2.png) [@Eneg](https://discuss.python.org/u/Eneg)\
**Post date:** [May 16, 2025, 1:08am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/24 "2025-05-16T01:08:52Z")

</div>

In [$ Freezing Function Objects](https://github.com/TobiasWrigstad/peps/blob/main/pep-9999.rst#freezing-function-objects):

```py
from immutable import freeze

def example2():
    x = 0
    def foo(a = False):
        if a:
            a = a + 1 # Note: updating local variables work, even in a frozen function
            return a
        else:
            x = x + 1
            return x

    freeze(foo)
    foo(41) # OK, returns 42
    foo() # Throws NotWriteableError

example2()

```

Doesn’t the call to `foo()` fail due to `UnboundLocalError` (with or without freezing)?  
The presence of the assignment `x = x + 1` makes `x` local to `foo`, consequently causing `x + 1` to fail in finding `x` in `foo`’s body before performing the assignment.  
The example needs `nonlocal x` for this to work as expected.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [May 16, 2025, 1:47am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/25 "2025-05-16T01:47:14Z")

</div>

> [@xFrednet](#):
>
> A less clean solution would be to use a check `isfrozen(self)` in ` __hash__ ()` to dynamically restrict the hash implementation to immutable objects.

This won’t work because a type is determined to be unhashable only if its ` __hash__ ` attribute is `None`. This means that the ` __hash__ ` attribute of an unhashable mutable type can’t be made a dynamic method to make an instance hashable when frozen.

> [@xFrednet](#):
>
> Or maybe have something like ` __frozen_hash__ ()`?

Yes, I think this is the way to go, and to make `isinstance(obj, Hashable)` work for frozen objects we can make changes to `abc` and `collections.abc` such that:

1. `abc.ABCMeta` offers not just a ` __subclasshook__ ` but also an ` __instancehook__ ` that is called by its ` __instancecheck__ ` before falling back to calling ` __subclasscheck__ `.
2. `collections.abc.Hashable` overrides the ` __instancehook__ ` to look for `type(obj). __frozen_hash__ ` if `isfrozen(obj)` is true.

This allows `isinstance(obj, Hashable)` to take the state of the instance itself into account. Without the change it currently can only check its type.

---

<div class="post-metadata">

**Author:** ![mjp41](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mjp41/32/27482_2.png) [@mjp41](https://discuss.python.org/u/mjp41)\
**Post date:** [May 16, 2025, 8:11am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/26 "2025-05-16T08:11:31Z")

</div>

> [@Eneg](#):
>
> In [$ Freezing Function Objects](https://github.com/TobiasWrigstad/peps/blob/main/pep-9999.rst#freezing-function-objects):
> 
> ```py
> from immutable import freeze
> 
> def example2():
> x = 0
> def foo(a = False):
> if a:
> a = a + 1 # Note: updating local variables work, even in a frozen function
> return a
> else:
> x = x + 1
> return x
> 
> freeze(foo)
> foo(41) # OK, returns 42
> foo() # Throws NotWriteableError
> 
> example2()
> 
> ```
> 
> Doesn’t the call to `foo()` fail due to `UnboundLocalError` (with or without freezing)?  
> The presence of the assignment `x = x + 1` makes `x` local to `foo`, consequently causing `x + 1` to fail in finding `x` in `foo`’s body before performing the assignment.  
> The example needs `nonlocal x` for this to work as expected.

You are correct we will update the document, thanks. With `nonlocal` the prototype works as expected.

---

<div class="post-metadata">

**Author:** ![stw](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/stw/32/27841_2.png) [@stw](https://discuss.python.org/u/stw)\
**Post date:** [May 19, 2025, 10:06am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/27 "2025-05-19T10:06:59Z")

</div>

Thanks for that suggestion — we will move it to a deferred ideas. Possibly also a discussion on typing can be moved there, a post on that to appear here shortly.

---

<div class="post-metadata">

**Author:** ![matajoh](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/matajoh/32/28001_2.png) [@matajoh](https://discuss.python.org/u/matajoh)\
**Post date:** [May 19, 2025, 1:11pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/28 "2025-05-19T13:11:30Z")

</div>

Thanks for the question! In our current implementation, we have (broadly) three scenarios as pertain to hashability:

1. An object graph which contains pure Python classes and currently hashable types
2. As above, but containing CPython classes which have been correctly made freezable (e.g. `list`, `dict`)
3. As above, but containing Python classes that wrap unfreeable CPython types but are registered as freezable (the wrapper/opt-in option for handling legacy CPython code)

In scenarios 1 and 2, I feel that what you propose is entirely doable. In scenario 3, it is probably not safe. Thankfully, we can detect which of these scenarios we are in fairly trivially, and ideally over time graphs in scenario 3 should decrease in number as various CPython codebases are updated to integrate more cleanly with this work. That said, I concur with Fred that this would be best suited for a follow-up PEP, as it would require some minor changes to the hashing logic.

---

<div class="post-metadata">

**Author:** ![matajoh](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/matajoh/32/28001_2.png) [@matajoh](https://discuss.python.org/u/matajoh)\
**Post date:** [May 19, 2025, 1:23pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/29 "2025-05-19T13:23:32Z")

</div>

There are several possible options here I think, again thinking of what a future PEP about this might entail. One can imagine adding a ` __ishashable__ ` method and a default ` __hash__ ` implementation that use immutability checks, which could act as an alternative to the current system for testing hashability. Something like that would have to be done carefully given the (potentially) wide-reaching implications. Still, given the potential benefit I for one would love to explore the possibilities here.

---

<div class="post-metadata">

**Author:** ![matajoh](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/matajoh/32/28001_2.png) [@matajoh](https://discuss.python.org/u/matajoh)\
**Post date:** [May 19, 2025, 1:35pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/30 "2025-05-19T13:35:38Z")

</div>

> [@sirosen](#):
>
> I need to read the document more, but my area of interest right now with the proposed freeze API is how we can reason about the scope of the changes it makes. e.g., if I freeze an object, could that result in freezing the module in which it or its class was defined?

This question (regarding the scope of freezing) is one which we’ve returned to multiple times during the course of this work. Our current philosophy is that we want, wherever possible, to minimise surprise. It is always possible to create, for lack of a better word, malicious object graphs which result in freezing a large number of things. Just simply calling `freeze(sys)` is going to result in a “good” time being had by all. We have tried to wargame some of these scenarios in our reference implementation to avoid language-breaking from occurring. For example, we manually omit some things from freezing to avoid breaking module import. We also make everything mutable during finalisation.

To your larger point, however, there remains this question of how to communicate to the programmer what is happening when they call `freeze`. The mental model we want for the programmer is that the object they passed will not change, both in content or behaviour, for the remainder of the program’s execution. Furthermore, our internal model is that the object becomes threadsafe, and able to used and interacted with by multiple threads concurrently without the possibility of a data race.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [May 19, 2025, 2:23pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/31 "2025-05-19T14:23:23Z")

</div>

I find the fundamental approach here a bit worrying. Frozen and non-frozen objects should be distinct types. The following code should not be able to fail for any reason except out-of-memory IMO:

```python
if isinstance(l, list):
    l.append(1)

```

Changing this invariant is such a fundamental change in the mental model programmers need to have that it IMO requires equally fundamental justifications, if it can ever happen. I have not seen any such justifications yet. In rust, every _variable_ has tracked mutability states - this is not what is being added in this PEP, what is being added is every _object_ having a tracked mutability state.

---

<div class="post-metadata">

**Author:** ![tstefan](https://avatars.discourse-cdn.com/v4/letter/t/f475e1/32.png) [@tstefan](https://discuss.python.org/u/tstefan)\
**Post date:** [May 19, 2025, 3:39pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/32 "2025-05-19T15:39:15Z")

</div>

I have a non-technical remark:

There are already [_frozen modules_](https://docs.python.org/3/library/importlib.html#importlib.machinery.FrozenImporter) in Python. Those modules are _not_ immutable at runtime, so _not_ frozen in the sense of this proposal. Maybe it was good, if a new name was invented.

---

<div class="post-metadata">

**Author:** ![loic-simon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/loic-simon/32/37461_2.png) [@loic-simon](https://discuss.python.org/u/loic-simon)\
**Post date:** [May 19, 2025, 8:41pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/33 "2025-05-19T20:41:07Z")

</div>

> [@MegaIng](#):
>
> The following code should not be able to fail for any reason except out-of-memory IMO:

This code can already easily fail:

```py
class L(list):
    def append(self, item): 
        raise ValueError("boom")

l = L()

```

So I’m not sure a frozen list would be _that_ different from a list subclass, conceptually. The fact that the `type(l) is list` guard won’t work either (IIUC) may be is worrysome, though.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [May 19, 2025, 8:58pm UTC](https://discuss.python.org/t/adding-deep-immutability/92011/34 "2025-05-19T20:58:28Z")

</div>

> [@loic-simon](#):
>
> This code can already easily fail:
> 
> ```python
> class L(list):
> def append(self, item): 
> raise ValueError("boom")
> 
> l = L()
> 
> ```
> 
> So I’m not sure a frozen list would be _that_ different from a list subclass, conceptually. The fact that the `type(l) is list` guard won’t work either (IIUC) may be is worrysome, though.

Yes, but that assumes a class almost malicious providing a subclass that breaks the Liskov Substitution Principle - which is mostly a safe assumption for python (with the very notable exception of hashability ofcourse).

This is contrasted with this proposal which adds a _builtin way_ to do this for _any object_.

I don’t think it would fly if someone suggested to add a new `ImmutableList` `list`-subtype somewhere in the stdlib, let alone deeply integrated into the interpreter.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [May 20, 2025, 1:32am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/35 "2025-05-20T01:32:22Z")

</div>

> [@MegaIng](#):
>
> I find the fundamental approach here a bit worrying. Frozen and non-frozen objects should be distinct types. The following code should not be able to fail for any reason except out-of-memory IMO:
> 
> ```python
> if isinstance(l, list):
> l.append(1)
> 
> ```
> 
> Changing this invariant is such a fundamental change in the mental model programmers need to have that it IMO requires equally fundamental justifications, if it can ever happen. I have not seen any such justifications yet. In rust, every _variable_ has tracked mutability states - this is not what is being added in this PEP, what is being added is every _object_ having a tracked mutability state.

I don’t think it’s going to be an issue. When we call a function we already know full well what the function does, including whether or not it is going to mutate an argument. So if a function does `if isinstance(l, list): l.append(1)` where `l` is an argument we already know from the docs not to pass to it an object that we don’t want to be altered. That’s already how it is today. We are already mentally tracking mutability ourselves now. Adding an assurance of immutability to an object at the language level only helps us ensure that it behaves in the way we expect it to.

We will also have further programmatic assurance when we are allowed to type an immutable parameter with a special form of `Immutable` in a separate proposal.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [May 20, 2025, 2:09am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/36 "2025-05-20T02:09:36Z")

</div>

> [@tstefan](#):
>
> I have a non-technical remark:
> 
> There are already [_frozen modules_](https://docs.python.org/3/library/importlib.html#importlib.machinery.FrozenImporter) in Python. Those modules are _not_ immutable at runtime, so _not_ frozen in the sense of this proposal. Maybe it was good, if a new name was invented.

Good point. The verb “freeze” is already used in Python to describe a type of module packaging.

I suppose we can instead use the invented word “immutablize”, already used to describe the act of making an object immutable by some [modules](https://www.rubydoc.info/gems/chef/Chef/Node/Immutablize) and [recipes](https://code.activestate.com/recipes/577831-to-change-instance-to-immutable/).

---

<div class="post-metadata">

**Author:** ![a-reich](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/a-reich/32/5885_2.png) [@a-reich](https://discuss.python.org/u/a-reich)\
**Post date:** [May 20, 2025, 2:17am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/37 "2025-05-20T02:17:18Z")

</div>

OTOH there is also `frozenset` denoting a set-like immutable object, so both meanings exist in the language. (If anything, frozenset is likely more commonly known than frozen modules).

---

<div class="post-metadata">

**Author:** ![sirosen](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/sirosen/32/8478_2.png) [@sirosen](https://discuss.python.org/u/sirosen)\
**Post date:** [May 20, 2025, 2:57am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/38 "2025-05-20T02:57:07Z")

</div>

> [@MegaIng](#):
>
> Changing this invariant is such a fundamental change in the mental model programmers need to have that it IMO requires equally fundamental justifications, if it can ever happen. I have not seen any such justifications yet.

I think you have seen the justification, which is “threadsafe programming is really hard”. So hard that people have invented new language models to handle it.

You don’t need to be convinced – I’m undecided about it, for whatever my opinion is worth – but I don’t think you’re going to find a much greater depth of explanation here.

Let’s take your example, list.append . I agree with you that anything today which breaks the safety of that code is, itself, fundamentally broken. If you created a list which can’t append, you shouldn’t have made it a list. Convincing `isinstance` to agree with you is totally facile – today, if it can’t append, it’s not a list.

This proposal changes that. It says that we’re allowed to “deny list” all mutators on an object. That makes it a new and different type for most people’s (and type systems’) purposes, but Python can’t represent it as such. It will have a new “immutable tag” that we can check at runtime.  
IANATT\[1\], but I’m sure the fancy higher order type systems are able to represent this sort of thing.

We can declare that for a codebase which includes freezing, the following snippet is wrong:

```python
if isinstance(l, list):
    l.append(1)

```

It “should be”:

```python
if not isfrozen(l) and isinstance(l, list):
    l.append(1)

```

Code which was correct has been rendered incorrect. That’s to be avoided where possible, but acceptable if there’s a commensurate benefit.

I’m pretty uncertain about how this could all shape up in terms of DX. In the worst case, it becomes difficult for library maintainers because they have no historical contact about immutability and suddenly need to invent one.  
How will programmers keep immutable objects from escaping the walled garden in which they were created and meant to be kept? What happens to libraries which assumed they could do things like sorting a list in-place? Their APIs become “not frozen safe”, but what does that look like in reality for under resourced maintainers who won’t even know about this feature until their users file bug reports?

* * *

1. I Am Not A Type Theorist

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [May 20, 2025, 3:29am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/39 "2025-05-20T03:29:03Z")

</div>

> [@sirosen](#):
>
> Code which was correct has been rendered incorrect. That’s to be avoided where possible, but acceptable if there’s a commensurate benefit.
> 
> I’m pretty uncertain about how this could all shape up in terms of DX. In the worst case, it becomes difficult for library maintainers because they have no historical contact about immutability and suddenly need to invent one.  
> How will programmers keep immutable objects from escaping the walled garden in which they were created and meant to be kept? What happens to libraries which assumed they could do things like sorting a list in-place? Their APIs become “not frozen safe”, but what does that look like in reality for under resourced maintainers who won’t even know about this feature until their users file bug reports?

The only way the existing libraries can break is if someone passes an immutable object to a function that is known to mutate the object. But why would anyone do that? You wouldn’t pass to such a function an object you don’t want to be altered today, so why would you want to do that when you have an object that is guaranteed by the language to be immutable? It should not change anything about how you call these existing library functions.

If anything, having an immutability guarantee from the language helps us prevent accidentally passing objects we don’t want altered to functions that do mutate them so we can catch such errors further upstream.

---

<div class="post-metadata">

**Author:** ![cben](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/cben/32/872_2.png) [@cben](https://discuss.python.org/u/cben)\
**Post date:** [May 20, 2025, 8:36am UTC](https://discuss.python.org/t/adding-deep-immutability/92011/40 "2025-05-20T08:36:48Z")

</div>

> [@stw](#):
>
> 2nd PEP: Add support for managing cyclic immutable garbage using a shared reference counter per strongly connected component of immutable objects plus adding support for atomic reference counting of immutable objects. This will permit immutable objects to be shared across threads and also across subinterpreters (by reference).

How do _immutable_ objects become cyclic? And is it common for these use cases?  
The PEP section on [simplified GC](https://github.com/TobiasWrigstad/peps/blob/main/pep-9999.rst#simplified-garbage-collection-for-immutable-object-graphs) doesn’t elaborate, though the example in Abstract shows that cycles could be formed before freezing.

- Is your stance merely that cycles are _possible_ so must be handled correctly?
- But if you’re saying SCC counts are an _optimization opportunity_, I think it’s worth motivating how common it is / what’s the best-case benefit.

[Previous page](https://discuss.python.org/t/adding-deep-immutability/92011.md?page=1)

[Next page](https://discuss.python.org/t/adding-deep-immutability/92011.md?page=3)
