2

The Setting

I have a data object that comprises a number of properties, let's say:

FaceProperties( eye_color:EyeColorEnum, has_glasses:bool, 
                nose_length_mm:float, hair_style:str )
  • All of these are optional, so instances may only define a subset of them.
  • For each single version of the software, there is a fixed set of supported properties (trying to set/get any other would be an implementation error)
  • It is quite likely that I need to add a new one to future versions every now and then

What happened so far

In principle, I would very much like to collect them in a dict, so that I can do all the nice things one can do with a dict (extend with new keys, iterate over, access by key...). However, an ordinary dict (and linter) would not notice when an unsupported key is used, e.g. due to some typo, especially with keys that are strings.

Due to these concerns, I started the project using an ordinary class with ordinary properties and implemented all the generic stuff (iteration etc) manually.

Are there better options?

I am currently refactoring the software, and started wondering if a map with a fixed key type (for which I could use a SupportedProperties Enum) would solve my problem better, and if there is such a thing in python.

Now, I can simply use the Enum values as keys with an ordinary dict and probably be reasonably fine, but it would be even nicer if there was some specialized dict class that would not even accept keys of other types.

Is there maybe some collection (preferably a builtin one) I could use for that? Or would I have to derive my own class from UserDict and do the type checking myself?

  • 1
    It really wouldn't be very hard to do it yourself. ```if key is not allowed: raise Exception()``` – Joshua Nixon Jul 25 '19 at 11:51
  • Do you mean to ask how you could restrict the keys of a dict? Or what alternatives there are to that? – 9769953 Jul 25 '19 at 11:51
  • Going by your penultimate paragraph: you could use typing support, but that generally only warns. But that is, in a way, inherent to Python: it's just that flexible. – 9769953 Jul 25 '19 at 11:55
  • Possible duplicate of [How can I represent an 'Enum' in Python?](https://stackoverflow.com/questions/36932/how-can-i-represent-an-enum-in-python) – norok2 Jul 25 '19 at 11:56
  • 1
    Sounds like just an upfront check gives you everything you need. When an input is passed that should not be accepted, just reject it by checking whether the keys are in sync. After that point, everyone should know what's what, and trying to prevent people from shooting themselves in the foot would be more hassle than worth. Just add one check against the dict being passed upfront, and you're good to go. – Paritosh Singh Jul 25 '19 at 11:56
  • 1
    @norok2 Umh, no. – Markus Meskanen Jul 25 '19 at 12:03
  • There is now [PEP 589](https://www.python.org/dev/peps/pep-0589/) which introduces `TypedDict` as "Type Hints for Dictionaries with a Fixed Set of Keys" (but it requires Python 3.8+) – ExternalCompilerError Jan 13 '20 at 08:06

2 Answers2

2

How about dataclasses (Python 3.7+)?

from enum import Enum
from dataclasses import dataclass, asdict


class EyeColorEnum(Enum):
    BLUE = 1
    RED = 2


@dataclass()
class FaceProperties:
    eye_color: EyeColorEnum = None
    has_glasses: bool = None
    nose_length_mm: float = None
    hair_style: str = None


fp = FaceProperties(eye_color=EyeColorEnum.BLUE)
print(fp)
print(asdict(fp))

outputs

FaceProperties(eye_color=<EyeColorEnum.BLUE: 1>, has_glasses=None, nose_length_mm=None, hair_style=None)
{'eye_color': <EyeColorEnum.BLUE: 1>, 'has_glasses': None, 'nose_length_mm': None, 'hair_style': None}
AKX
  • 152,115
  • 15
  • 115
  • 172
  • While this doesn't solve all my problems (e.g. iteration does not seem to be built-in, also, I am currently stuck with python 3.6), thanks for pointing those out, clearly relevant. No idea why someone voted this down. – ExternalCompilerError Jul 25 '19 at 12:47
  • 1
    There's a backport for Py3.6. https://github.com/ericvsmith/dataclasses – AKX Jul 25 '19 at 12:49
2

As others have mentioned in the comments, all you need is one check to ensure that the key is in a certain set of values:

class FixedKeyDict(dict):
    def __init__(self, *args, allowed_keys=(), **kwargs):
        super().__init__(*args, **kwargs)
        self.allowed_keys = allowed_keys

    def __setitem__(self, key, value):
        if key not in self.allowed_keys:
            raise KeyError('Key {} is not allowed'.format(key))
        super().__setitem__(key, value)

Now give it a list of the allowed values:

hair_dict = FixedKeyDict(allowed_keys=('bald', 'wavy', 'straight', 'mohawk'))

This only overrides the [] operator though, you want to subclass UserDict like you've said and override all the other methods too, but you get the idea. Regardless, you must implement it yourself, such thing doesn't exist in the standard library.

Markus Meskanen
  • 19,939
  • 18
  • 80
  • 119
  • True, it is not too hard. However, your example would better show its use as `face_dict = FixedKeyDict( allowed_keys=('eye_color', 'has_glasses', 'nose_length_mm', 'hair_style'))`, as for hair styles, an Enum does nicely. – ExternalCompilerError Jul 26 '19 at 07:33