Here's a more elaborate fork of the Marquinho Peli's answer. Changes: support for dictionaries with nested lists;
class DotDict(dict):
    """A dictionary that's recursively navigable with dots, not brackets."""
    def __init__(self, data: dict = None):
        super().__init__()
        if data is None or not isinstance(data, dict):
            raise AttributeError(f"{type(self).__name__} must be instantiated with a dictionary, not a {type(data).__name__}.")
        for key, value in data.items():
            if isinstance(value, list):
                self[key] = [DotDict(item) for item in value]
            elif isinstance(value, dict):
                self[key] = DotDict(value)
            else:
                self[key] = value
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"attribute .{key} not found")
Usage examples:
d = {'key1': 'value1',
     'key2': 'value2',
     'key3': {'key3a': 'value3a'},
     'key4': {'key4a': [{'key4aa': 'value4aa',
                         'key4ab': 'value4ab',
                         'key4ac': 'value4ac'}],
              'key4b': 'value4b'}}
dd = DotDict(d)
print(dd.key4.key4a[0].key4aa)  # value4aa
dd.key4.key4a[0].key4aa = 'newval'
print(dd.key4.key4a[0].key4aa)  # newval
print(dd.key4.key4a[0].key4aaa) # AttributeError: attribute .key4aaa not found
DotDict({}) # OK
DotDict()   # AttributeError: DotDict must be instantiated with dictionary, not a NoneType.