The Details

Instead of dict, use rjgtoys.thing.Thing.

It has a constructor that works exactly like dict, so it can be a drop-in replacement:

from rjgtoys.thing import Thing

x = Thing(a=1, b=2)

y = Thing((('a',1), ('b',2)))

If you access an item that doesn’t exist, you get KeyError and if you access an attribute that doesn’t exist, you get AttributeError.

Converting dict to Thing

When you construct a Thing from a dict, all may not be well:

data = {
   'a': 1,
   'b': {
      'c': {
         'd': 3
       }
    }
 }

data_thing = Thing(data)

The resulting data_thing will indeed be a Thing but the constructor does not do a ‘deep copy’ or even a ‘deep conversion’, and so the internal dict elements will remain as dict.

That will defeat any attempt to use attribute access past the first level:

part_b = data_thing.b   # This is fine

d_value = data_thing.b.c.d  # This will fail; data_thing.b is a dict

The solution is to use from_object(). That does a ‘deep copy’ and will convert any internal dict objects into Thing

data_thing = Thing.from_object(data)

Other Awkward Cases and Caveats

Items don’t hide existing attributes

Items that have the same name as an existing attribute of dict are not available via attribute access: items is one example; even for Thing it is still the method that returns the sequence of key-value pairs. So if you build this:

data = Thing(
   items=[1,2,3]
 )

You will still have to access the data as data['items'], whilst data.items() will return you the sequence (('items', [1,2,3])).

Dots in item names are not usable with attribute access

If your Thing has items with names containing dots, you can always access them using item-style access data['a.b.c'] but attribute style access is not possible.

Item names with dots cannot always be concatenated with others

For simple-named item names, you can use ‘paths’ to navigate through nested objects:

data = {
   'a': 1,
   'b': {
      'c': {
         'd': 3
       }
    }
 }

data_thing = Thing.from_object(data)

data_thing.b.c.d == 3    # This works
data_thing['b.c.d']      # So does this

But this fails when the item names themselves contain dots:

data = {
   'a': 1,
   'b.c': {
         'd': 3
       }
    }
 }

data_thing = Thing.from_object(data)

data_thing.b.c.d == 3    # FAILS
data_thing['b.c.d']      # FAILS

data_thing['b.c']['d']   # Works (but it would work with a dict too)