Skip to content

How to Access Dictionary Keys Using Dot Notation

Learn how to enable dot notation (e.g., my_dict.key) for Python dictionaries using custom classes, __getattr__, or existing libraries like Box for cleaner attribute-style access.

Python dictionaries are fundamental data structures, typically accessed using square bracket notation (e.g., my_dict['key']). However, developers coming from other languages (like JavaScript) or those seeking a different syntax sometimes desire attribute-style dot notation (e.g., my_dict.key).

While standard Python dictionaries don't support this directly, this guide demonstrates several techniques, primarily using class inheritance and special methods like __getattr__ or __dict__, to enable dot notation access for dictionary-like objects.

Standard Dictionary Access ([]) vs. Dot Notation (.)

By default, Python differentiates between accessing attributes of an object using dot notation (object.attribute) and accessing items within a container (like a dictionary or list) using square brackets (container['key'] or container[index]). Dictionaries are item-based containers, hence the standard [] access. Trying my_dict.key on a regular dictionary usually results in an AttributeError because dictionaries don't typically have attributes matching their keys.

This approach involves creating a custom class that inherits from dict and overrides special methods to translate attribute access (.) into item access ([]).

Basic Dot Notation for Getting Items

We can override the __getattr__ method. This special method is called by Python only when an attempt to access an attribute using dot notation fails in the usual way (i.e., the attribute doesn't actually exist on the object). We redirect this failed attribute lookup to a dictionary item lookup (__getitem__).

class AttributeDictGet(dict):
  """A dict subclass enabling read access via dot notation."""
  __getattr__ = dict.__getitem__ # type: ignore # Ignore potential type hint issues

my_data = {'id': 101, 'status': 'active', 'user': 'admin'}
attr_dict = AttributeDictGet(my_data)

print(f"Status: {attr_dict.status}")          # Output: Status: active
print(f"User: {attr_dict.user}")              # Output: User: admin

print(f"ID (bracket): {attr_dict['id']}")     # Output: ID (bracket): 101

try:
    print(attr_dict.non_existent)
except KeyError as e:
    print(f"Error getting non_existent: {e}") # Output: Error getting non_existent: 'non_existent'

Enabling Setting and Deleting via Dot Notation

To allow setting (attr_dict.new_key = value) and deleting (del attr_dict.key) using dot notation, we similarly redirect __setattr__ and __delattr__.

class AttributeDict(dict):
  """A dict subclass enabling get/set/del via dot notation."""
  __getattr__ = dict.__getitem__ # type: ignore

  __setattr__ = dict.__setitem__

  __delattr__ = dict.__delitem__

data = {'name': 'Alice', 'city': 'London'}
attr_dict = AttributeDict(data)

print(f"Name: {attr_dict.name}") # Output: Name: Alice

attr_dict.country = 'UK' # Triggers __setattr__ -> __setitem__
print(f"After set: {attr_dict}")

del attr_dict.city # Triggers __delattr__ -> __delitem__
print(f"After delete: {attr_dict}")

This class now behaves much like a JavaScript object regarding property access.

Handling Non-Existent Keys (KeyError vs. None)

The basic __getattr__ = dict.__getitem__ approach raises a KeyError if you try to access a non-existent key via dot notation (because __getitem__ raises KeyError). If you prefer it to return None (similar to dict.get()), you can redirect __getattr__ to dict.get.

class AttributeDictGetNone(dict):
  """A dict subclass returning None for missing keys via dot access."""
  __getattr__ = dict.get # type: ignore
  __setattr__ = dict.__setitem__
  __delattr__ = dict.__delitem__

data = {'known_key': 'value1'}
attr_dict_none = AttributeDictGetNone(data)

print(f"Known key: {attr_dict_none.known_key}")     # Output: Known key: value1
print(f"Missing key: {attr_dict_none.missing_key}") # Output: Missing key: None

try:
    print(attr_dict_none['missing_key'])
except KeyError as e:
    print(f"Bracket access still raises: {e}") # Output: Bracket access still raises: 'missing_key'

Choose the __getattr__ target (__getitem__ vs get) based on the desired behavior for missing keys accessed via dot notation.

Method 2: Using __dict__ (Attribute-Dictionary Mapping - Use with Caution)

This less common technique involves directly linking the object's instance attribute dictionary (__dict__) to the object itself (which inherits from dict).

Implementation

class AttributeDictFromDict(dict):
  """Enables dot notation by merging attribute and item namespaces."""
  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs) # Initialize the dict part
    self.__dict__ = self

data = {'id': 205, 'type': 'event', 'payload': [1, 2]}
attr_dict_dd = AttributeDictFromDict(data)

print(f"ID: {attr_dict_dd.id}")           # Output: ID: 205
print(f"Payload: {attr_dict_dd.payload}") # Output: Payload: [1, 2]

attr_dict_dd.status = 'processed'
print(f"After set: {attr_dict_dd}")

Consequence: AttributeError for Missing Keys

A key difference with this method is that accessing a non-existent key via dot notation now raises an AttributeError, not a KeyError.

try:
  print(attr_dict_dd.non_existent)
except AttributeError as e:
  print(f"Error accessing non_existent: {e}")

This happens because Python looks for non_existent in attr_dict_dd.__dict__, which is attr_dict_dd itself, doesn't find the key/attribute, and raises AttributeError.

Potential Pitfalls

Warning: The __dict__ = self method can lead to unexpected behavior and bugs because it merges the namespace for dictionary items with the namespace for object attributes and methods.

  • Method/Attribute Clashes: If your dictionary has a key with the same name as a built-in dictionary method (e.g., 'keys', 'items', 'update') or any method/attribute you define on the AttributeDictFromDict class itself, accessing that name via dot notation might give you the dictionary value instead of the method, or vice-versa, potentially causing errors.
  • Less Predictable: It breaks the clear distinction between accessing object attributes and accessing dictionary items.

For these reasons, the __getattr__ approach (Method 1) is generally considered safer and more robust.

Method 3: Handling Nested Dictionaries Recursively

The methods above only work for the top level of the dictionary. If you have nested dictionaries and want dot notation all the way down (e.g., data.user.address.city), you need a recursive approach, often implemented without inheriting from dict.

class Struct:
  """Creates an object allowing dot notation access, including nested dicts."""
  def __init__(self, data_dict):
    for key, value in data_dict.items():
      if isinstance(value, dict):
          setattr(self, key, Struct(value))
      else:
          setattr(self, key, value)

  def __repr__(self):
    attrs = vars(self)
    attr_str = ', '.join(f'{k}={v!r}' for k, v in attrs.items())
    return f'{self.__class__.__name__}({attr_str})'

nested_data = {
    'id': 'P100',
    'details': {
        'name': 'Gadget',
        'location': {
            'warehouse': 'WH-A',
            'shelf': 'S3'
        },
        'tags': ['tech', 'new']
    },
    'status': 'available'
}

obj = Struct(nested_data)

print(f"ID: {obj.id}")                                # Output: ID: P100
print(f"Name: {obj.details.name}")                    # Output: Name: Gadget
print(f"Warehouse: {obj.details.location.warehouse}") # Output: Warehouse: WH-A
print(f"Tags: {obj.details.tags}")                    # Output: Tags: ['tech', 'new']
print(f"\nObject representation: {obj}")

try:
    print(obj.details.price)
except AttributeError as e:
    print(f"\nError accessing price: {e}")
  • This Struct class takes a dictionary and converts its keys into attributes.
  • If a value is itself a dictionary, it recursively creates another Struct instance for it.
  • This doesn't inherit from dict, so standard dict methods aren't available directly on the obj.

Alternatives: Existing Libraries

Instead of reinventing the wheel, several third-party libraries provide robust implementations of dictionaries with attribute-style access, often handling edge cases and offering more features:

  • Box (pip install python-box): A popular choice, handles nested access well.
  • addict (pip install addict): Another library for attribute access.
  • munch (pip install munch): Similar functionality.
  • types.SimpleNamespace (built-in): While not a dictionary subclass, you can initialize it with keyword arguments to get simple attribute access (doesn't handle nested dicts automatically).
    from types import SimpleNamespace
    ns = SimpleNamespace(name='Alice', age=30)
    print(ns.name) # Output: Alice

For production code or complex use cases, using one of these battle-tested libraries is often recommended over rolling your own.

Choosing the Right Method

  • Method 1 (__getattr__): Generally recommended if you want a dictionary subclass. It keeps attribute and item access distinct, provides clear error handling (KeyError vs. None via get), and is less prone to namespace collisions than Method 2.
  • Method 2 (__dict__): Use with extreme caution. Only suitable for simple cases where you are certain keys won't clash with method names and understand the AttributeError consequence.
  • Method 3 (Recursive Struct): Necessary if you require nested dot notation access. Note that it creates regular objects, not dictionary subclasses.
  • Existing Libraries: Often the best choice for production code, providing robust, well-tested implementations.

Conclusion

While standard Python dictionaries use square brackets ([]) for item access, you can achieve attribute-style dot notation (.) access by:

  1. Creating a custom dict subclass and redirecting __getattr__ (and optionally __setattr__, __delattr__) to the corresponding dictionary methods (__getitem__ or get, __setitem__, __delitem__). This is the most robust custom approach.
  2. (Use cautiously) Directly mapping self.__dict__ = self in a dict subclass, which merges namespaces and changes error types.
  3. Using a recursive class structure for nested dot notation.
  4. Leveraging existing third-party libraries like Box or addict for feature-rich, reliable solutions.

Choose the technique that best balances your need for dot notation with considerations for error handling, nested access, and potential namespace conflicts.

Navigation