The Dangers of Misusing __dir__ and @property in Python: An Anti-Pattern Exposed

Python is an elegant and dynamic language that gives developers tools like __dir__ and @property to build intuitive and flexible APIs. These features let us dynamically customize behavior or compute attribute values as needed. But with great power comes great responsibility.

I’ve seen—and even encountered myself—a recurring anti-pattern where developers use these constructs to perform heavy operations, such as making network or gRPC calls. While it might seem like a clever shortcut, this practice often leads to subtle bugs, performance issues, and an unpleasant developer experience.

In this post, I’ll explain the purpose of __dir__ and @property, why embedding heavy operations in them is dangerous, and how to use them correctly.

What Is __dir__?

The __dir__ method is a special method in Python that customizes the behavior of the dir() function. When you call dir() on an object, Python lists all its attributes, including dynamically added ones if __dir__ is implemented.

What __dir__ Is Meant For

The purpose of __dir__ is to aid introspection, primarily for debugging and development tools. A correctly implemented __dir__ should:

  • Return a lightweight, complete, and accurate list of an object’s attributes.
  • Avoid side effects, such as modifying state or making network calls.
  • Be fast, as dir() is often called automatically by tools like IDEs and debuggers.

Example of Proper Use

class ConfigManager:
    def __init__(self):
        # Dynamic configurations
        self.config = {
            'debug': True,
            'timeout': 30,
            'theme': 'light',
        }

    def __getattr__(self, name):
        # Allow dynamic access to configuration keys as attributes
        if name in self.config:
            return self.config[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __dir__(self):
        # Show only dynamic attributes (config keys) for introspection
        return list(self.config.keys())


# Example usage
config = ConfigManager()

# Accessing configuration values dynamically
print(config.debug)    # Output: True
print(config.timeout)  # Output: 30

# Introspecting available configuration attributes
print(dir(config))
# Output: ['debug', 'timeout', 'theme']

Why This Is Useful

Justification: It simplifies the developer experience by making dir() show only the data the user needs to work with.

Clean Introspection: Instead of showing irrelevant attributes or internal methods (e.g., __init__, __dict__), dir() focuses on the meaningful attributes—in this case, the configuration keys.

The Anti-Pattern: Using API Calls in __dir__

Embedding network calls in __dir__ is problematic because:

  1. dir() Is Implicitly Called: Tools like help(obj), debuggers, and IDEs frequently call dir(). Making __dir__ depend on a network request can cause unintended slowdowns or failures.
  2. Unexpected Side Effects: Developers don’t expect dir() to perform operations like API calls. If something breaks, tracing the issue can be a nightmare.
  3. Performance and Reliability: Network requests are inherently slower and less reliable than local operations. Introducing them into a method like __dir__ is a recipe for poor performance.

What Is @property?

The @property decorator is a Python feature that allows a method to be accessed like an attribute. This is incredibly useful for dynamically computed values.

What @property Is Meant For

Properties are great for:

  • Encapsulating logic behind an attribute, such as lazy computations or validation.
  • Providing read-only attributes.
  • Abstracting away implementation details.

Example of Proper Use

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

Here, the area property dynamically calculates its value based on the circle’s radius. This is exactly what @property is meant for.

The Anti-Pattern: Using gRPC or API Calls in @property

Embedding a gRPC or API call in a property might seem clever, but it introduces serious issues:

  1. Misleading Simplicity: Developers expect properties to be lightweight. A property that hides a network call can confuse others who use your code—or even yourself later.
  2. Repeated Calls: If the property is accessed multiple times in a loop or a debugger, it will repeatedly trigger the network call, leading to performance bottlenecks.
  3. Debugging Pain: If the network call fails (e.g., timeout), you’ll have a seemingly simple property throwing exceptions.

Comparing the Two: __dir__ vs. @property

Both __dir__ and @property can lead to issues when misused, but the impact is slightly different:

Aspect__dir__@property
PurposeIntrospection for attributesDynamically calculated attributes
How It’s TriggeredImplicit (e.g., dir(obj) or tools)Explicit (e.g., obj.some_property)
ExpectationLightweight, no side effectsTypically lightweight, but can include logic
Impact of MisuseCan break tools like IDEs and debuggersCan cause performance issues in code
DiscoverabilityHidden (called by tools, not developers)Obvious (called explicitly in code)

How to Avoid the Anti-Pattern

For __dir__

  • Keep It Lightweight: Use __dir__ only for introspection logic, such as appending dynamically added attributes.
  • Avoid Side Effects: Don’t modify state or perform external operations like network calls.
  • Cache Results If Necessary: If you must fetch data (e.g., dynamic attribute names), fetch it once and reuse it.

For @property

  • Use Explicit Methods for Heavy Operations: If retrieving the value involves a gRPC or API call, provide an explicit method like fetch_data() instead of hiding the operation behind a property.
  • Cache Results: If a property must fetch remote data, cache the result to avoid repeated calls.
  • Document Behavior: Make it clear in your documentation if a property is performing significant operations.

The Right Way to Implement Dynamic Behavior

Example: Explicit Method for gRPC Calls

class RemoteObject:
    def __init__(self):
        self._data = None

    def fetch_data(self):
        # Explicitly fetch data from the server
        self._data = grpc_client.get_data()

    @property
    def data(self):
        if self._data is None:
            raise ValueError("Data not loaded. Call fetch_data() first.")
        return self._data

Example: Cached Property

class RemoteObject:
    def __init__(self):
        self._data = None

    @property
    def data(self):
        if self._data is None:
            self._data = grpc_client.get_data()  # Cache the result
        return self._data

Why You Should Never Use __dir__ or @property to Fetch Remote Data

As developers, we rely on Python’s built-in features like __dir__ and @property to make our code dynamic and intuitive. These tools allow us to customize behavior and encapsulate logic, making our APIs feel elegant and seamless. But with these powerful tools comes the temptation to use them in ways that break the fundamental expectations developers have. One particularly dangerous anti-pattern I’ve encountered is embedding remote data fetches—like API or gRPC calls—inside these constructs.

Breaking Expectations

Let’s start with the obvious: dir() is expected to be a lightweight tool for introspection. It’s something developers use in the shell, IDEs, and debuggers to quickly list the attributes of an object. When __dir__ is modified to include a remote API call, it violates this expectation entirely. Suddenly, dir(obj) becomes a potential source of network delays or failures. Tools that implicitly rely on dir()—such as autocompletion in an IDE—may slow down dramatically or even break if the network connection isn’t stable. This behavior can leave developers scratching their heads, trying to figure out why their perfectly valid introspection is causing timeouts or errors.

Similarly, @property is expected to represent a computed value or a lightweight encapsulation of data, not a potentially slow and unreliable remote operation. When a property is used to fetch data from a remote server, it creates a disconnect between what the user sees (a simple attribute) and what’s actually happening (a heavy operation). Worse, if the property is accessed multiple times, it may result in repeated network calls, which can degrade performance and introduce unnecessary complexity.

Unintended State Changes

Another insidious danger is unintended state changes. Imagine a @property that fetches data from a server and triggers some server-side logic as a result. Or a __dir__ implementation that modifies state while trying to fetch attribute names dynamically. These side effects are completely counterintuitive. A developer calling dir() or accessing a property expects to retrieve information, not to change the state of the system. When these constructs behave unpredictably, it becomes almost impossible to debug or maintain the code.

Performance and Reliability

Fetching remote data inside __dir__ or @property also introduces performance issues and reliability concerns. Network operations are inherently slower and less predictable than local computations. If a remote server goes down or the network connection is unstable, it could render even basic operations—like inspecting an object with dir()—unusable. This creates a brittle system where seemingly innocuous actions depend on external factors that are out of the developer’s control.

The Real Danger: Trust

Ultimately, the biggest issue is trust. Developers trust dir() to be safe and fast. They trust properties to behave like attributes. By embedding remote data fetches in these constructs, you’re breaking that trust. It’s not just about technical performance; it’s about creating APIs that are intuitive, predictable, and enjoyable to use. When __dir__ or @property starts doing things that no one expects, you erode the confidence developers have in your code. And that’s a problem no clever workaround can fix.

By avoiding this anti-pattern and respecting the intended use of __dir__ and @property, we can ensure our code remains clean, reliable, and a joy to work with. Let’s keep these tools lightweight, side-effect-free, and aligned with the expectations developers rely on. Trust me, your future self—and your fellow developers—will thank you.

Final Thoughts

Python’s flexibility makes it a joy to use, but it also requires careful thought when designing APIs. Misusing __dir__ and @property to perform heavy operations like network calls is an anti-pattern that can lead to subtle bugs and frustrated developers. By understanding the intended purposes of these features and following best practices, we can write code that’s intuitive, efficient, and a pleasure to use.

If you’ve encountered (or fallen into) this anti-pattern before, I’d love to hear your experiences. Let’s share lessons and build better Python code together!

Leave a Comment