1

Problem

Similar to previous questions, I would like to create a frozen/immutable dictionary. Specifically, after initialization, the user should get an ValueError when trying to use the __delitem__ and __setitem__ methods.

Unlike the previous questions, I specifically want it to be a sub-class where the initialization type is constrained to a specific key and value type.

Attempted Solution

My own attempts at accomplishing this with collections.UserDict failed:

class WorkflowParams(UserDict):
    def __init__(self, __dict: Mapping[str, str]) -> None:
        super().__init__(__dict=__dict)

    def __setitem__(self, key: str, item: str) -> None:
        raise AttributeError("WorkflowParams is immutable.")

    def __delitem__(self, key: str) -> None:
        raise AttributeError("WorkflowParams is immutable.")

When trying to use it:

workflow_parameters = WorkflowParams(
    {
        "s3-bucket": "my-big-bucket",
        "input-val": "1",
    }
)

It fails with

Traceback (most recent call last):
  File "examples/python_step/python_step.py", line 38, in <module>
    workflow_parameters = WorkflowParams(
  File "/home/sean/git/scargo/scargo/core.py", line 14, in __init__
    super().__init__(__dict=__dict)
  File "/home/sean/miniconda3/envs/scargo/lib/python3.8/collections/__init__.py", line 1001, in __init__
    self.update(kwargs)
  File "/home/sean/miniconda3/envs/scargo/lib/python3.8/_collections_abc.py", line 832, in update
    self[key] = other[key]
  File "/home/sean/git/scargo/scargo/core.py", line 17, in __setitem__
    raise AttributeError("WorkflowParams is immutable.")
AttributeError: WorkflowParams is immutable.

Because of how __init__() resolves methods.

Disqualified Alternatives

Because of my need for a subclass, the commonly suggested solution of using MappingProxyType doesn't meet my requirements.

Additionally, I'm suspicious of answers which recommend subclassing dict, since this seems to cause some unintended behaviour.

wim
  • 338,267
  • 99
  • 616
  • 750
Seanny123
  • 8,776
  • 13
  • 68
  • 124
  • Well, whatever you pass to ` super().__init__(__dict=__dict)` will eventually call `__setitem__`, why don't you just manually do that in your `__init__` using `super().__setitiem__`? – juanpa.arrivillaga Feb 03 '21 at 23:20
  • @juanpa.arrivillaga somehow, that also doesn't work. `super().__setitem__(key, __dict[key])` still calls my subclass' `__setitem__`. – Seanny123 Feb 03 '21 at 23:35
  • oh woops, yeah, because it's an abstract method. I mean, just use `self.data[key] = value` – juanpa.arrivillaga Feb 03 '21 at 23:37
  • @juanpa.arrivillaga `self.data` isn't a class attribute, so it doesn't exist until after `UserDict.__init()__` is called. – Seanny123 Feb 03 '21 at 23:41
  • I just tried to call super().__setitem__() as part of the constructor and it all seem to have worked as intended. Can you advise with what Python version that doesn't seem to work? Also why wouldn't you want to call UserDict.__init__()? – de1 Feb 04 '21 at 00:09

2 Answers2

3

This seem to work just fine for me (tested with Python 3.6 and 3.8):

from collections import UserDict
from typing import Mapping


class WorkflowParams(UserDict):
    def __init__(self, __dict: Mapping[str, str]) -> None:
        super().__init__()
        for key, value in __dict.items():
            super().__setitem__(key, value)

    def __setitem__(self, key: str, item: str) -> None:
        raise AttributeError("WorkflowParams is immutable.")

    def __delitem__(self, key: str) -> None:
        raise AttributeError("WorkflowParams is immutable.")


workflow_parameters = WorkflowParams(
    {
        "s3-bucket": "my-big-bucket",
        "input-val": "1",
    }
)

print(workflow_parameters)
# output: {'s3-bucket': 'my-big-bucket', 'input-val': '1'}

workflow_parameters['test'] = 'dummy'
# expected exception: AttributeError: WorkflowParams is immutable.
de1
  • 2,986
  • 1
  • 15
  • 32
1

I would do it with the collections.abc, which to just to quickly build container classes, just implement a couple of thing and done

>>> import collections
>>> class FrozenDict(collections.abc.Mapping):

    def __init__(self,/,*argv,**karg):
        self._data = dict(*argv,**karg)

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

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

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

    def __repr__(self):
        return f"{type(self).__name__}({self._data!r})"

    
>>> t=FrozenDict( {
        "s3-bucket": "my-big-bucket",
        "input-val": "1",
    })
>>> t
FrozenDict({'s3-bucket': 'my-big-bucket', 'input-val': '1'})
>>> t["test"]=23
Traceback (most recent call last):
  File "<pyshell#38>", line 1, in <module>
    t["test"]=23
TypeError: 'FrozenDict' object does not support item assignment
>>> del t["input-val"]
Traceback (most recent call last):
  File "<pyshell#39>", line 1, in <module>
    del t["input-val"]
TypeError: 'FrozenDict' object does not support item deletion
>>> 
Copperfield
  • 8,131
  • 3
  • 23
  • 29