Python - getattr/setattr/hasattr Functions

Python's dot notation works perfectly when you know attribute names at write time. But what happens when attribute names come from user input, configuration files, or database records? You can't...

Key Insights

  • getattr, setattr, and hasattr enable dynamic attribute access when you can’t hardcode attribute names—essential for plugin systems, configuration loaders, and serialization frameworks.
  • Always provide a default value to getattr when the attribute might not exist; otherwise, you’ll face AttributeError exceptions that can crash your application.
  • hasattr internally calls getattr and catches exceptions, which means it can mask errors in property getters or __getattr__ methods—use it carefully in production code.

Introduction to Attribute Access Functions

Python’s dot notation works perfectly when you know attribute names at write time. But what happens when attribute names come from user input, configuration files, or database records? You can’t write obj.user_provided_string because Python interprets that literally.

That’s where getattr, setattr, and hasattr come in. These built-in functions let you manipulate object attributes using strings, enabling dynamic behavior that static code can’t achieve. They’re the foundation of plugin architectures, ORM systems, and configuration frameworks throughout the Python ecosystem.

Understanding these functions transforms how you think about Python objects. Instead of rigid structures with predetermined attributes, objects become flexible containers you can inspect and modify at runtime.

getattr() - Retrieving Attributes Dynamically

The getattr function retrieves an attribute from an object using a string name. Its signature is straightforward:

getattr(object, name[, default])

The third parameter is crucial. Without it, getattr raises AttributeError for missing attributes. With it, you get the default value instead.

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

user = User("Alice", "alice@example.com")

# Basic attribute access
print(getattr(user, "name"))  # Output: Alice

# Equivalent to dot notation
print(user.name)  # Output: Alice

# Without default - raises AttributeError
try:
    getattr(user, "phone")
except AttributeError as e:
    print(f"Error: {e}")  # Output: Error: 'User' object has no attribute 'phone'

# With default - returns default value safely
phone = getattr(user, "phone", "Not provided")
print(phone)  # Output: Not provided

The real power emerges when attribute names come from external sources:

def get_user_field(user, field_name):
    """Retrieve any user field by name from config or user input."""
    allowed_fields = {"name", "email", "department", "role"}
    
    if field_name not in allowed_fields:
        raise ValueError(f"Field '{field_name}' is not accessible")
    
    return getattr(user, field_name, None)

# From configuration file
config = {"display_field": "email"}
field_to_show = config["display_field"]
value = get_user_field(user, field_to_show)
print(f"Displaying: {value}")  # Output: Displaying: alice@example.com

setattr() - Setting Attributes Dynamically

While getattr reads attributes, setattr writes them:

setattr(object, name, value)

This function creates attributes if they don’t exist or updates them if they do. It’s invaluable for building objects from external data sources.

class Config:
    """Configuration object built from dictionary data."""
    pass

def build_config_from_dict(data: dict) -> Config:
    """Create a Config object with attributes from dictionary keys."""
    config = Config()
    for key, value in data.items():
        setattr(config, key, value)
    return config

# From JSON/YAML configuration
settings = {
    "database_host": "localhost",
    "database_port": 5432,
    "debug_mode": True,
    "max_connections": 100
}

config = build_config_from_dict(settings)
print(config.database_host)  # Output: localhost
print(config.debug_mode)     # Output: True

Here’s a more sophisticated example that loads configuration with type validation:

class AppConfig:
    """Application configuration with type-checked dynamic loading."""
    
    SCHEMA = {
        "host": str,
        "port": int,
        "debug": bool,
        "timeout": float,
    }
    
    @classmethod
    def from_dict(cls, data: dict) -> "AppConfig":
        instance = cls()
        for key, expected_type in cls.SCHEMA.items():
            if key in data:
                value = data[key]
                if not isinstance(value, expected_type):
                    raise TypeError(
                        f"'{key}' must be {expected_type.__name__}, "
                        f"got {type(value).__name__}"
                    )
                setattr(instance, key, value)
            else:
                setattr(instance, key, None)
        return instance

config = AppConfig.from_dict({
    "host": "api.example.com",
    "port": 8080,
    "debug": False,
    "timeout": 30.0
})

hasattr() - Checking Attribute Existence

The hasattr function checks whether an object has a specific attribute:

hasattr(object, name)  # Returns True or False

Internally, hasattr calls getattr and returns False if AttributeError is raised. This implementation detail matters because it affects how you should use it.

class FeatureFlags:
    def __init__(self):
        self.dark_mode = True
        self.beta_features = False

flags = FeatureFlags()

# Feature detection pattern
if hasattr(flags, "dark_mode") and flags.dark_mode:
    print("Dark mode enabled")

if hasattr(flags, "experimental_ai"):
    print("AI features available")
else:
    print("AI features not configured")

Python developers often debate LBYL (Look Before You Leap) versus EAFP (Easier to Ask Forgiveness than Permission). Here’s both approaches:

# LBYL with hasattr
def get_display_name_lbyl(user):
    if hasattr(user, "display_name"):
        return user.display_name
    elif hasattr(user, "name"):
        return user.name
    return "Anonymous"

# EAFP with try/except
def get_display_name_eafp(user):
    try:
        return user.display_name
    except AttributeError:
        pass
    try:
        return user.name
    except AttributeError:
        return "Anonymous"

# Hybrid approach (often cleanest)
def get_display_name_hybrid(user):
    return getattr(user, "display_name", None) or getattr(user, "name", "Anonymous")

The hybrid approach using getattr with defaults often produces the cleanest code while maintaining safety.

Practical Use Cases

These functions shine in real-world scenarios. Here’s a plugin loader that dynamically instantiates handlers:

class BaseHandler:
    def handle(self, data):
        raise NotImplementedError

class JSONHandler(BaseHandler):
    def handle(self, data):
        return f"Processing JSON: {data}"

class XMLHandler(BaseHandler):
    def handle(self, data):
        return f"Processing XML: {data}"

class CSVHandler(BaseHandler):
    def handle(self, data):
        return f"Processing CSV: {data}"

class HandlerRegistry:
    """Dynamic handler loader using getattr."""
    
    def __init__(self):
        self._handlers = {}
    
    def register(self, format_name: str, handler_class):
        self._handlers[format_name] = handler_class
    
    def get_handler(self, format_name: str) -> BaseHandler:
        handler_class = self._handlers.get(format_name)
        if handler_class is None:
            raise ValueError(f"No handler for format: {format_name}")
        return handler_class()

# Usage
registry = HandlerRegistry()
registry.register("json", JSONHandler)
registry.register("xml", XMLHandler)
registry.register("csv", CSVHandler)

# Dynamic dispatch based on file extension
file_format = "json"  # Could come from filename or config
handler = registry.get_handler(file_format)
result = handler.handle('{"key": "value"}')
print(result)  # Output: Processing JSON: {"key": "value"}

Here’s a generic serializer that converts objects to dictionaries:

def serialize_object(obj, fields: list[str] = None) -> dict:
    """Convert object attributes to dictionary."""
    if fields is None:
        # Get all public attributes
        fields = [attr for attr in dir(obj) 
                  if not attr.startswith("_") 
                  and not callable(getattr(obj, attr))]
    
    return {
        field: getattr(obj, field, None)
        for field in fields
    }

class Product:
    def __init__(self, id, name, price, category):
        self.id = id
        self.name = name
        self.price = price
        self.category = category

product = Product(1, "Laptop", 999.99, "Electronics")
data = serialize_object(product, ["id", "name", "price"])
print(data)  # Output: {'id': 1, 'name': 'Laptop', 'price': 999.99}

Working with __getattr__ and __setattr__

Python’s attribute access functions connect to magic methods that let you customize attribute behavior entirely. The __getattr__ method is called when normal attribute lookup fails, while __setattr__ intercepts all attribute assignments.

class APIProxy:
    """Proxy that converts attribute access to API calls."""
    
    def __init__(self, base_url: str):
        # Use object.__setattr__ to avoid recursion
        object.__setattr__(self, "_base_url", base_url)
        object.__setattr__(self, "_cache", {})
    
    def __getattr__(self, name: str):
        """Convert attribute access to API endpoint lookup."""
        if name.startswith("_"):
            raise AttributeError(f"Private attribute: {name}")
        
        # Simulate API call
        endpoint = f"{self._base_url}/{name}"
        print(f"Fetching from: {endpoint}")
        
        # Cache and return mock response
        if name not in self._cache:
            self._cache[name] = {"endpoint": endpoint, "data": f"Response for {name}"}
        return self._cache[name]
    
    def __setattr__(self, name: str, value):
        """Intercept attribute setting for validation."""
        if name.startswith("_"):
            object.__setattr__(self, name, value)
        else:
            print(f"Would POST to {self._base_url}/{name}: {value}")

# Usage
api = APIProxy("https://api.example.com")
users = api.users      # Output: Fetching from: https://api.example.com/users
products = api.products  # Output: Fetching from: https://api.example.com/products
api.orders = {"item": "laptop"}  # Output: Would POST to https://api.example.com/orders: ...

Best Practices and Pitfalls

Use direct access when possible. Don’t write getattr(obj, "name") when obj.name works. Dynamic access adds overhead and reduces code clarity. Reserve these functions for genuinely dynamic scenarios.

Always validate attribute names from external sources. Never pass unsanitized user input directly to these functions:

# Dangerous - allows access to any attribute
def bad_get_field(obj, field_name):
    return getattr(obj, field_name)

# Safe - whitelist allowed fields
def safe_get_field(obj, field_name, allowed_fields):
    if field_name not in allowed_fields:
        raise ValueError(f"Access to '{field_name}' not permitted")
    return getattr(obj, field_name, None)

Be aware that hasattr can hide errors. If an object has a property that raises an exception, hasattr returns False rather than propagating the error:

class Problematic:
    @property
    def broken(self):
        raise RuntimeError("Something went wrong")

obj = Problematic()
print(hasattr(obj, "broken"))  # Output: False (hides the RuntimeError!)

Consider performance in tight loops. While the overhead is minimal for occasional use, getattr is slower than direct attribute access. In performance-critical code processing millions of objects, the difference adds up.

These functions are fundamental tools for building flexible, dynamic Python applications. Master them, and you’ll write more adaptable code that handles real-world complexity with elegance.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.