9

I want to subclass dict in python such that all the dictionaries of the sub-class are immutable.

I don't understand how does __hash__ affects the immutability, since in my understanding it just signifies the equality or non-equality of objects !

So, can __hash__ be used to implement immutability ? How ?

Update:

Objective is that common response from an API is available as a dict, which has to be shared as a global variable. So, that needs to be intact no matter what ?

Yugal Jindle
  • 41,867
  • 41
  • 126
  • 194
  • This has been asked many times before (unfortunately, I don't have a link handy right now). For most problems, trying to make an immutable dictionary is the wrong approach. What exactly are you trying to achieve? – Sven Marnach Jun 13 '12 at 11:54
  • 3
    How about this link: http://stackoverflow.com/q/2703599/623518 – Chris Jun 13 '12 at 11:55
  • I would just override the `__setitem__()` method of your dict. Note however, that this doesn't guarantee that the values of your dict will be immutable (say you have values that are lists, for example). – Joel Cornett Jun 13 '12 at 12:06
  • 2
    @JoelCornett: You would at least also need to overwrite `__delitem__()`, `clear()`, `pop()`, `popitem()`, `setdefault()` and `update()`. – Sven Marnach Jun 13 '12 at 12:16
  • @SvenMarnach: hahaha I forgot about those XD. Yes you would. – Joel Cornett Jun 13 '12 at 12:23
  • @SvenMarnach Is there a simpler way? How come `tuple` is immutable ? – Yugal Jindle Jun 13 '12 at 12:30
  • @YugalJindle: Whether there is a simpler way to achieve what you are trying to achieve depends on what you are trying to achieve. Most probably, yes! – Sven Marnach Jun 13 '12 at 12:36
  • This might be of interest: https://github.com/magicstack/immutables – Superdooperhero Dec 28 '19 at 08:48

6 Answers6

10

I found a Official reference : suggestion contained in a rejected PEP.

class imdict(dict):
    def __hash__(self):
        return id(self)

    def _immutable(self, *args, **kws):
        raise TypeError('object is immutable')

    __setitem__ = _immutable
    __delitem__ = _immutable
    clear       = _immutable
    update      = _immutable
    setdefault  = _immutable
    pop         = _immutable
    popitem     = _immutable

Attribution : http://www.python.org/dev/peps/pep-0351/

Mike
  • 56,266
  • 74
  • 170
  • 215
Yugal Jindle
  • 41,867
  • 41
  • 126
  • 194
6

So, can __hash__ be used to implement immutability ?

No, it can't. The object can be made mutable (or not) irrespective of what its __hash__ method does.

The relationship between immutable objects and __hash__ is that, since an immutable object cannot be changed, the value returned by __hash__ remains constant post-construction. For mutable objects, this may or may not be the case (the recommended practice is that such objects simply fail to hash).

For further discussion, see Issue 13707: Clarify hash() constency period.

NPE
  • 464,258
  • 100
  • 912
  • 987
  • Well, user-defined classes are both mutable and hashable by default, but the hash will remain constant anyway! – Sven Marnach Jun 13 '12 at 11:57
  • @SvenMarnach: What you're saying is true. What I'm saying is also true. TBH, I don't see where your line of reasoning is going... – NPE Jun 13 '12 at 12:03
  • I'm simply suggesting that the last sentence of your answer is somewhat incomplete. There's more to the relationship between immutability and hashability. – Sven Marnach Jun 13 '12 at 12:09
  • Obviously, I need to be more specific. A hash value that might change over time is quite useless in Python. The main purpose of hashes is that hashable objects may be used in sets and dictionaries. If the hash value changes, this will fail in strange and unpredictable ways. So *all* sensible hash implementations have the property that the hash value remains constant, no exceptions. – Sven Marnach Jun 13 '12 at 12:13
  • Can you provide some sample code `How to write an immutable dict ?` – Yugal Jindle Jun 13 '12 at 12:30
5

Regarding the relationship between hashability and mutability:

To be useful, a hash implementation needs to fulfil the following properties:

  1. The hash value of two objects that compare equal using == must be equal.

  2. The hash value may not change over time.

These two properties imply that hashable classes cannot take mutable properties into account when comparing instances, and by contraposition that classes which do take mutable properties into account when comparing instances are not hashable. Immutable classes can be made hashable without any implications for comparison.

All of the built-in mutable types are not hashable, and all of the immutable built-in types are hashable. This is mainly a consequence of the above observations.

User-defined classes by default define comparison based on object identity, and use the id() as hash. They are mutable, but the mutable data is not taken into account when comparing instances, so they can be made hashable.

Making a class hashable does not make it immutable in some magic way. On the contrary, to make a dictionary hashable in a reasonable way while keeping the original comparison operator, you will first need to make it immutable.

Edit: Regarding your update:

There are several ways to provide the equivalent of global immutable dictionary:

  1. Use a collections.namedtuple() instance instead.

  2. Use a user-defined class with read-only properties.

  3. I'd usually go with something like this:

    _my_global_dict = {"a": 42, "b": 7}
    
    def request_value(key):
        return _my_global_dict[key]
    

    By the leading underscore, you make clear that _my_global_dict is an implementation detail not to be touched by application code. Note that this code would still allow to modify dictionary values if they happen to be mutable objects. You could solve this problem by returning copy.copy()s or copy.deepcopy()s of the values if necessary.

Sven Marnach
  • 530,615
  • 113
  • 910
  • 808
  • So, please give some code that can make it `immutable` ? Since this answers just half the question ! – Yugal Jindle Jun 13 '12 at 12:46
  • 1
    @YugalJindle: As I said numerous times before, using an immutable dictionary is most probably the wrong solution for your problem, but we can only tell you if you tell us what your problem actually is. (Moreover, I get the impression that you are not really interested in a good solution, and that you don't really pay attention to the answers and comments. Otherwise, you would already have found some example code, which is linked in the comments above.) – Sven Marnach Jun 13 '12 at 12:49
  • @YugalJindle: No, it's certainly not the only solution in your case. And again, the code is for an immutable dictionary linked in the second comment to your question. – Sven Marnach Jun 13 '12 at 13:00
  • I really don't understand how does your code implements a immutable dict. You wrote a wrapper method for key query, but I want to give it away as a proper dict object that can perform everything except write operations. I said in the question `subclass`. Also, I can't use deepcopy since its very slow. – Yugal Jindle Jun 13 '12 at 13:14
  • 1
    @YugalJindle: Sorry for disobeying your orders. I don't think I can help you any further. Good luck! – Sven Marnach Jun 13 '12 at 13:27
  • I will be looking for the solution. `SO` is a place for helping not ordering.. Thanks for your immense out of the point efforts ! – Yugal Jindle Jun 14 '12 at 01:58
2

In frozendict, hash is simply implemented following the rejected PEP 416:

def __hash__(self):
    try:
        fs = frozenset(self.items())
    except TypeError:
        hash = -1
    else:
        hash = hash(fs)
    
    if hash == -1:
        raise TypeError("Not all values are hashable.")
    
    return hash

PS: I'm the new maintainer of the package.

Marco Sulla
  • 14,337
  • 13
  • 54
  • 92
0

Since Python 3.3, it's possible to use MappingProxyType to create an immutable mapping:

>>> from types import MappingProxyType
>>> MappingProxyType({'a': 1})
mappingproxy({'a': 1})
>>> immutable_mapping = MappingProxyType({'a': 1})
>>> immutable_mapping['a']
1
>>> immutable_mapping['b'] = 2
Traceback (most recent call last):
  (...)
TypeError: 'mappingproxy' object does not support item assignment

It's not hashable so you can't use it as a dictionary key (and it's "final", so you can't subclass it to override __hash__), but it's good enough if you want an immutable mapping to prevent accidental modification of a global value (like a class default attribute).

Careful not to add mutable values that could themselves be modified.

LeoRochael
  • 12,383
  • 5
  • 26
  • 37
0

It is possible to create immutable dict using just standard library.

from types import MappingProxyType

power_levels = MappingProxyType(
    {
        "Kevin": 9001,
        "Benny": 8000,
    }
)

See source of idea with more detailed explanation

Konstantin Smolyanin
  • 16,105
  • 12
  • 51
  • 46