40

I have a class like:

class A:
    def __init__(self):
        self.data = {}

and at some moment I want to prohibit self.data fields modification.

I've read in PEP-416 rejection notice that there are a lot of ways to do it. So I'd like to find what they are.

I tried this:

a = A()
a.data = types.MappingProxyType(a.data)

That should work but first, its python3.3+ and second, when I do this "prohibition" multiple times I get this:

>>> a.data = types.MappingProxyType(a.data)
>>> a.data = types.MappingProxyType(a.data)
>>> a.data
mappingproxy(mappingproxy({}))

though it would be much better to get just mappingproxy({}) as I am going to "prohibit" a lot of times. Check of isinstance(MappingProxyType) is an option, but I think that other options can exist.

Thanks

martineau
  • 119,623
  • 25
  • 170
  • 301
sshilovsky
  • 958
  • 2
  • 9
  • 22
  • 1
    http://stackoverflow.com/questions/8752451/how-to-change-the-behavior-of-a-python-dictionarys-setattr may help you – Garfield Sep 26 '13 at 08:29
  • 3
    A frozendict implementation looks rather trivial: https://github.com/slezica/python-frozendict/blob/master/frozendict/__init__.py – georg Sep 26 '13 at 08:36
  • 2
    @codelover Thats right, I can override `__setitem__` with `throw NotImplemented`. Is there guarantee that there will be no way to modify keys or values via any standard `dict` attributes in any python implementation? – sshilovsky Sep 27 '13 at 06:28
  • 1
    @sshilovsky Nope, instead you could derive an user dict and override whatever & you can use where ever it is applicable. – Garfield Sep 27 '13 at 12:52
  • 2
    Does this answer your question? [What would a "frozen dict" be?](https://stackoverflow.com/questions/2703599/what-would-a-frozen-dict-be) – Seanny123 Feb 03 '21 at 23:18

6 Answers6

46

Use collections.Mapping e.g.

import collections

class DictWrapper(collections.Mapping):

    def __init__(self, data):
        self._data = data

    def __getitem__(self, key): 
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)
NoDataDumpNoContribution
  • 10,591
  • 9
  • 64
  • 104
mouad
  • 67,571
  • 18
  • 114
  • 106
  • Can it be that this only makes the dict itself, but not necessarilty contained dicts immutable? I currently want to make a nested dict representing a config immuatable. – Martin Thoma Apr 07 '17 at 15:34
  • 1
    I think it works exactly like you describe already. `dict_wrapper[key]` returns whatever you've put there, which might as well be a raw `dict`. – sshilovsky Jun 22 '22 at 13:12
  • Great answer! It's worth mentioning: (1) Modern python wants `collections.abc.Mapping`, not `collections.Mapping` which has been deprecated. (2) `collections.abc.Mapping` simply doesn't have `__setitem__` or ilk methods defined, so attempts to store data to the mapping will result in a `TypeError` exception. (3) If you want something to make an entire tree of dictionaries read-only, you need to make `__gititem__` return a read-only wrapper for lists and dicts. Additionally, you'll need to define a read-only iterator for `__iter__` to return. – Jonathan Mayer Oct 12 '22 at 17:28
38

This is the full implementation of fast (shallow-)read-only dict:

def _readonly(self, *args, **kwargs):
    raise RuntimeError("Cannot modify ReadOnlyDict")

class ReadOnlyDict(dict):
    __setitem__ = _readonly
    __delitem__ = _readonly
    pop = _readonly
    popitem = _readonly
    clear = _readonly
    update = _readonly
    setdefault = _readonly

My previous (worse) implementation was as follows (thanks @mtraceur for the great remarks!):

class ReadOnlyDict(dict):
    def __readonly__(self, *args, **kwargs):
        raise RuntimeError("Cannot modify ReadOnlyDict")
    __setitem__ = __readonly__
    __delitem__ = __readonly__
    pop = __readonly__
    popitem = __readonly__
    clear = __readonly__
    update = __readonly__
    setdefault = __readonly__
    del __readonly__
Marcin Raczyński
  • 944
  • 1
  • 10
  • 14
  • To allow for copying them using `copy.copy` and `copy.deepcopy` you might want to add the following two lines: `__copy__ = dict.copy` and `__deepcopy__ = copy._deepcopy_dispatch.get(dict)`. Not much tested though. – blueyed Nov 09 '17 at 15:42
  • right, but how to initialize this or how to make existing dict use this? – mari.mts Mar 26 '19 at 11:28
  • 4
    ```ro_dict = ReadOnlyDict(rw_dict)``` – Marcin Raczyński Mar 03 '20 at 12:53
  • 1
    Doing `del __readonly__` will mess with pickling of the methods, both unbound and bound, if that even comes up. Also all names of the form `__*__` are reserved by Python, and are free to break at any time. So I would call it `_readonly`, define it outside the class, and just leave it in the module it's defined in instead of `del`-ing it. (*Bound* which are that function will still be unpickleable, but that's due to a weirdly broken approach in Python's pickling of bound methods - unbound references to those methods will be pickleable.) – mtraceur Jul 04 '22 at 08:44
  • Cheers, just noticed the edit, happy to help! – mtraceur Oct 27 '22 at 04:16
  • this does not reflect updates, consider - https://gist.github.com/nduhamel/748954 instead – Dave Ankin Aug 01 '23 at 14:05
10

Very easy, you just override default dict's methods!
Here is an example:

class ReadOnlyDict(dict):

    __readonly = False

    def readonly(self, allow=1):
        """Allow or deny modifying dictionary"""
        self.__readonly = bool(allow)

    def __setitem__(self, key, value):

        if self.__readonly:
            raise TypeError, "__setitem__ is not supported"
        return dict.__setitem__(self, key, value)

    def __delitem__(self, key):

        if self.__readonly:
            raise TypeError, "__delitem__ is not supported"
        return dict.__delitem__(self, key)

BTW, you can also remove .pop, .update and other methods you need. Just play around with it.

JadedTuna
  • 1,783
  • 2
  • 18
  • 32
  • How do you apply this "at some moment"? I guess OP needs the dict to be read-write, then make it frozen after some time. – justhalf Sep 26 '13 at 09:15
  • If he needs, he can modify them too – JadedTuna Sep 26 '13 at 13:34
  • 4
    This approach is obvious but is not simple as I have to override almost each `dict` method and even reimplement `dict` views like `.keys()`, `.items()` etc. And what if Guido adds some more in future python versions? Anyway, thanks for the point, but I wish there were more answers. – sshilovsky Sep 27 '13 at 05:33
3

The best way is to derive from UserDict like this:

from collections import UserDict

class MyReadOnlyDict(UserDict):
   def my_set(self, key, val, more_params):
       # do something special
       # custom logic etc
       self.data[key] = val

   def __setitem__(self, key, val):
       raise NotImplementedError('This dictionary cannot be updated')

   def __delitem__(self, key):
       raise NotImplementedError('This dictionary does not allow delete')

The advantage of this method is that you can still have internal methods in your class that can update dictionary by accessing self.data.

Ouroborus
  • 16,237
  • 4
  • 39
  • 62
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
1

How about? It is the update of @mouad 's answer.

import json
from collections import OrderedDict
from collections.abc import Mapping


class ReadOnlyJsonObject(Mapping):
    def __init__(self, data, dumps_kw: dict=None, loads_kw: dict=None):
        if dumps_kw is None:
            dumps_kw = dict()
        if loads_kw is None:
            self._loads_kw = dict(object_pairs_hook=OrderedDict)
        else:
            self._loads_kw = loads_kw

        if isinstance(data, str):
            self._json_string = data
        else:
            self._json_string = json.dumps(data, **dumps_kw)

    @property
    def _data(self):
        return json.loads(self._json_string, **self._loads_kw)

    def __getitem__(self, key):
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def __str__(self):
        return self._json_string

Not sure about the performance, though. I use this one in a real project https://github.com/patarapolw/AnkiTools/blob/master/AnkiTools/tools/defaults.py

Polv
  • 1,918
  • 1
  • 20
  • 31
0

Belated answer. You can use a combination of property and MappingProxyType to protect the .data field from modifications.

from types import MappingProxyType

class A:
    def __init__(self):
        self._data = {}

    @property
    def data(self):
        return MappingProxyType(self._data)

    def update(self, key, value):
        self._data[key] = value

a = A()
a.update("foo", 1)
a.update("bar", 2)
print(f"a.data={a.data}")

# Attempt to modify a.data directly will fail
try:
    a.data["bar"] = 20
except TypeError as error:
    print(f"Error: {error}")

# Attempt to assign something else to a.data, will also fail
try:
    a.data = {}
except AttributeError as error:
    print(f"Error: {error}")

Output:

a.data={'foo': 1, 'bar': 2}
Error: 'mappingproxy' object does not support item assignment
Error: property 'data' of 'A' object has no setter

Notes

  • The A.data is not a regular attribute, it is a property. Since there is no setter, it is also a read-only property.
  • A.data returns a MappingProxy instead of the dictionary itself. That means code outside of the class will not be able to modify the dictionary's contents.
  • In order to modify the dictionary, I added the method update() as an example
  • If people want to, they can modify A._data directly, there is nothing we can do to prevent that.
Hai Vu
  • 37,849
  • 11
  • 66
  • 93