Python

`__getattr__` vs `__getattribute__` in Python

Learn the crucial difference between Python's `__getattr__` and `__getattribute__` methods, when to use each, and how to avoid common recursion traps with practical examples from production code.

July 2026 8 min read 11 views 0 hearts

Python's __getattr__ vs __getattribute__: The Difference That Can Make or Break Your Code

When I first started working with Python's attribute access methods, I'll admit I was confused. Both __getattr__ and __getattribute__ seem to do similar things - they intercept attribute lookups on objects. But the difference between them is crucial, and mixing them up can lead to some frustrating bugs.

Let me break this down in a way that actually makes sense.

The Core Difference

Think of __getattribute__ as the default route every attribute lookup takes. It runs every single time you access any attribute on an object. __getattr__, on the other hand, is the emergency backup - it only runs when normal attribute lookup fails.

Here's the simplest way to remember it: - __getattribute__ runs always (even if the attribute exists) - __getattr__ runs only when the attribute doesn't exist

Seeing It in Action

Let me show you a real example from PythonSkillset's debugging toolkit:

class DebugLogger:
    def __init__(self):
        self.log_count = 0

    def __getattribute__(self, name):
        print(f"Accessing: {name}")
        return object.__getattribute__(self, name)

    def __getattr__(self, name):
        print(f"Attribute {name} not found - creating default")
        return "default_value"

logger = DebugLogger()
print(logger.log_count)    # This calls __getattribute__
print(logger.nonexistent)  # This calls __getattribute__ first, then __getattr__

When you run this, you'll see that __getattribute__ fires for both accesses, but __getattr__ only kicks in for the second one.

Why This Matters in Practice

The practical implications are huge. Here's a scenario from PythonSkillset's configuration management system:

class ConfigWithDefaults:
    def __init__(self):
        self._config = {"host": "localhost", "port": 8080}

    def __getattr__(self, name):
        # Only runs when normal lookup fails
        if name.startswith("_"):
            raise AttributeError(name)
        return self._config.get(name, "default_value")

config = ConfigWithDefaults()
print(config.host)    # Hmm, this might cause recursion!

Notice the problem? If host doesn't exist as a direct attribute, __getattr__ runs, tries to access self._config, which calls __getattr__ again for _config... and boom, infinite recursion.

The Safe Way to Use These Methods

In PythonSkillset's production code, we always use this pattern:

class SafeConfig:
    def __init__(self):
        self._config = {"host": "localhost", "port": 8080}

    def __getattr__(self, name):
        # Use object.__getattribute__ for internal attributes
        if name.startswith("_"):
            raise AttributeError(f"{type(self).__name__} has no attribute {name}")
        try:
            return self._config[name]
        except KeyError:
            return "default_value"

    def __getattribute__(self, name):
        # Log all attribute access for debugging
        print(f"Accessing {name}")
        return object.__getattribute__(self, name)

When to Use Each One

Based on PythonSkillset's experience, here's my practical guidance:

Use __getattribute__ when: - You need to intercept every attribute access (logging, profiling) - You're building a proxy or wrapper pattern - You want to control access to existing attributes

Use __getattr__ when: - You're implementing a fallback or default mechanism - You want to dynamically create attributes - You're working with configuration or settings that have defaults

The Recursion Trap

This is the most common mistake I see. Consider this broken example:

# DON'T DO THIS
class Broken:
    def __getattr__(self, name):
        return self.something  # Recursion!

Every time __getattr__ tries to access self.something, it triggers itself again. Always use object.__getattribute__ for internal attribute access:

# DO THIS INSTEAD
class Working:
    def __getattr__(self, name):
        # Safe internal access
        internal = object.__getattribute__(self, '_data')
        return internal.get(name, "default")

Real World Example: Lazy Loading

Here's a pattern PythonSkillset uses for lazy-loaded database models:

class LazyModel:
    def __init__(self, model_name):
        self._model_name = model_name
        self._loaded = False
        self._data = {}

    def __getattr__(self, name):
        if not self._loaded:
            # Trigger lazy loading
            print(f"Loading {self._model_name} data...")
            self._load_data()
            self._loaded = True
        # Now try to get the attribute
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"{self._model_name} has no attribute '{name}'")

    def __getattribute__(self, name):
        # Log access for debugging (never intentionally)
        print(f"Accessing {name} on {object.__getattribute__(self, '_model_name')}")
        return object.__getattribute__(self, name)

    def _load_data(self):
        self._data = {"name": "User", "age": 30, "email": "user@example.com"}

What Actually Happens Under the Hood

When you write obj.attr, Python does this: 1. Calls type(obj).__getattribute__(obj, 'attr') 2. If that raises AttributeError, calls type(obj).__getattr__(obj, 'attr') 3. If __getattr__ also raises AttributeError, you get the normal error

This means __getattribute__ is the gatekeeper, and __getattr__ is the last resort.

My Final Advice

After years of PythonSkillset development, here's what I'd tell you:

  1. Start with __getattr__ - It's safer and solves 90% of use cases
  2. Only use __getattribute__ when you absolutely need to - It's powerful but dangerous
  3. Always use object.__getattribute__ for internal access in both methods
  4. Test edge cases - especially what happens with hasattr() and private attributes

The difference between these two methods might seem subtle, but understanding it will save you from some of the weirdest bugs you'll ever encounter in Python. Trust me, I've been there.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.