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.

5
  • 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__? Commented Feb 3, 2021 at 23:20
  • @juanpa.arrivillaga somehow, that also doesn't work. super().__setitem__(key, __dict[key]) still calls my subclass' __setitem__. Commented Feb 3, 2021 at 23:35
  • oh woops, yeah, because it's an abstract method. I mean, just use self.data[key] = value Commented Feb 3, 2021 at 23:37
  • @juanpa.arrivillaga self.data isn't a class attribute, so it doesn't exist until after UserDict.__init()__ is called. Commented Feb 3, 2021 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__()? Commented Feb 4, 2021 at 0:09

2 Answers 2

5

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.
Sign up to request clarification or add additional context in comments.

Comments

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
>>> 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.