Customizing __dir__ in Dynamic Python Objects

Customizing the __dir__ method in Python allows developers to define the exact list of attribute names returned by the built-in dir() function for dynamically constructed objects. In dynamic programming patterns—such as proxy objects, API wrappers, and schema-driven data containers—attributes are often resolved at runtime via __getattr__, making them invisible to Python's default introspection tools. Implementing a custom __dir__ method bridges this gap, enabling interactive debuggers, modern IDEs, and read-eval-print loops (REPLs) to accurately discover, inspect, and autocomplete dynamically generated attributes.

The Limitation of Default Introspection

By default, Python determines an object's available attributes by inspecting its instance dictionary (__dict__), its class dictionary, and any inherited attributes across its base classes. When using dynamic dispatch mechanisms like __getattr__, attributes do not physically exist in these namespaces until they are requested.

As a result, calling dir() on an instance of a class that dynamically resolves properties returns only the static, explicitly declared methods and standard dunder attributes. The dynamic attributes remain hidden from runtime inspection, impairing code discoverability.

Enabling IDE Autocompletion and Developer Usability

The primary function of __dir__ customization is enhancing the developer experience. Modern developer tooling—including VS Code, PyCharm, IPython, and Jupyter Notebooks—relies directly on the dir() function to populate tab-completion lists.

Without an overridden __dir__, users of a dynamic class must consult external documentation or inspect source code to know which attributes are accessible. By customizing __dir__, a library author can dynamically assemble and expose the names of attributes derived from external sources, such as JSON schemas, database columns, or remote API endpoints.

Implementation Example

To implement a custom __dir__, define a method named __dir__ on the class that returns an iterable of strings. The standard approach merges the object's default attributes with the dynamically resolved keys.

class DynamicRecord:
    def __init__(self, data: dict):
        self._data = data

    def __getattr__(self, name: str):
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

    def __dir__(self):
        # Retrieve standard attributes from the base implementation
        default_attrs = super().__dir__()
        # Combine default attributes with dynamic keys
        dynamic_attrs = list(self._data.keys())
        return sorted(set(default_attrs + dynamic_attrs))

In this implementation, calling dir(DynamicRecord({"name": "Alice", "role": "Admin"})) returns the standard object attributes alongside "name" and "role", making them immediately available to autocomplete engines.

Key Rules for Customizing __dir__

When overriding __dir__, adhere to the following conventions: