hasattr() Function Complexity¶
The hasattr() function checks whether an object has a named attribute. It is
one getattr() whose AttributeError becomes False, so it costs exactly
what the attribute lookup costs: a cached class-level lookup plus an instance
dict probe, or whatever a user-defined hook does.
Complexity Reference¶
Let d be the length of the object's class MRO and g the cost, in time and
space, of a user-defined hook: __getattr__, __getattribute__, or a
descriptor such as a property getter.
| Operation | Time | Space | Notes |
|---|---|---|---|
hasattr(obj, name) |
O(1) avg | O(1) | The class-level lookup of a plain str name is cached per class and name, for misses as well as hits, so d does not matter once the cache is warm |
hasattr(obj, name), first lookup after the class or a base changed |
O(d) | O(1) | Walks the MRO once and caches the result. From Python 3.13, once a class has been changed and looked up again more than 1,000 times it is never cached again, and every lookup on it pays O(d) |
hasattr(obj, name) through a hook |
O(g) | O(g) | On top of the lookup above. A property getter runs, and __getattr__ runs on every miss. Only AttributeError becomes False; any other exception propagates |
hasattr(obj, name) with a non-str name |
O(1) | O(1) | Raises TypeError before any lookup |
Basic Usage¶
Check Attribute Existence¶
# O(1) avg - one cached lookup
class MyClass:
attr = 42
obj = MyClass()
# Safe check - O(1) avg
if hasattr(obj, 'attr'):
print(obj.attr) # 42
# A miss is cached too - O(1) avg
if hasattr(obj, 'missing'):
print("Has missing")
else:
print("No missing attribute")
Avoid AttributeError¶
# All three forms do the same lookup; they differ in how many times they do it
class User:
name = "Alice"
user = User()
# try/except - one lookup
try:
email = user.email
except AttributeError:
email = "no@email.com"
# hasattr() then access - two lookups when the attribute exists
if hasattr(user, 'email'):
email = user.email
else:
email = "no@email.com"
# getattr() with a default - one lookup either way, and the clearest
email = getattr(user, 'email', "no@email.com")
Complexity Details¶
Inheritance Chain Traversal¶
# The first lookup of a name on a class walks the MRO - O(d)
# Later lookups of that name on that class are cached - O(1) avg
class A:
a_attr = 1
class B(A):
b_attr = 2
class C(B):
c_attr = 3
class D(C):
d_attr = 4
obj = D()
# MRO: [D, C, B, A, object]
has_a = hasattr(obj, 'a_attr') # O(d) once: D, C, B, then found in A
has_a = hasattr(obj, 'a_attr') # O(1) avg: cached for (D, 'a_attr')
has_x = hasattr(obj, 'x_attr') # O(d) once, then the miss is cached too
# Assigning to a class, or to any of its bases, invalidates its cache
C.c_attr = 30
has_a = hasattr(obj, 'a_attr') # O(d) again, once
What hasattr() Catches¶
# hasattr() is one getattr() call; only AttributeError means False
def hasattr_simulation(obj, name):
"""What hasattr() does - the cost is the lookup's"""
if not isinstance(name, str):
raise TypeError("attribute name must be string")
try:
getattr(obj, name)
except AttributeError:
return False
return True
class Strict:
@property
def broken(self):
raise KeyError("not an AttributeError")
obj = Strict()
print(hasattr(obj, 'missing')) # False
try:
hasattr(obj, 'broken') # the KeyError propagates
except KeyError:
print("only AttributeError means False")
try:
hasattr(obj, 42)
except TypeError:
print("the name must be a str")
Performance Patterns¶
hasattr vs getattr with Default¶
# hasattr() then getattr() - two lookups
class Config:
host = "localhost"
obj = Config()
if hasattr(obj, 'host'): # lookup one
value = getattr(obj, 'host') # lookup two
else:
value = None
# getattr() with a default - one lookup
value = getattr(obj, 'host', None)
Checking Multiple Attributes¶
# O(n) - one cached lookup per name checked
class Message:
to = "user@example.com"
subject = "Hello"
body = "Content"
msg = Message()
required = ['to', 'subject', 'body']
valid = all(hasattr(msg, attr) for attr in required) # O(n) - n names
# dir() is not a shortcut: it collects and sorts every attribute of the
# object and its whole MRO, so it grows with that attribute count where a
# few hasattr() calls do not
valid = {'to', 'subject', 'body'} <= set(dir(msg)) # at least O(a) in the a names listed
Common Use Cases¶
Optional Feature Detection¶
# O(1) avg - check if feature is available
class DataStore:
def load(self):
pass
def save(self):
pass
# backup() is optional
class DatabaseStore(DataStore):
def backup(self):
"""Advanced feature"""
pass
store = DatabaseStore()
# O(1) avg - check for optional method
if hasattr(store, 'backup'):
store.backup() # Call if available
Protocol Checking¶
# One cached lookup per protocol method
def is_iterable(obj):
"""Check for the iterator protocol - one lookup"""
return hasattr(obj, '__iter__')
def is_context_manager(obj):
"""Check for the context manager protocol - one or two lookups"""
return hasattr(obj, '__enter__') and hasattr(obj, '__exit__')
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
obj = Countdown(3)
if is_iterable(obj):
for item in obj:
print(item) # 3, 2, 1
Safe Method Invocation¶
# hasattr() then the call - two lookups when the method exists
class Handler:
def process(self, data):
return f"Processed: {data}"
class MinimalHandler:
pass
def safe_process(obj, data):
"""Call method if it exists"""
if hasattr(obj, 'process'): # O(1) avg
return obj.process(data)
return None
handler = Handler()
minimal = MinimalHandler()
result1 = safe_process(handler, "data") # Processed: data
result2 = safe_process(minimal, "data") # None
Advanced Usage¶
Dynamic Behavior Routing¶
# Route to a handler by name
class Plugin:
def on_start(self):
pass
def on_stop(self):
pass
class AdvancedPlugin(Plugin):
def on_update(self):
pass
def trigger_event(obj, event_name):
"""Safely trigger an event - one lookup"""
handler = getattr(obj, event_name, None)
if handler is not None:
handler()
plugin = AdvancedPlugin()
trigger_event(plugin, 'on_start') # Works
trigger_event(plugin, 'on_update') # Works
trigger_event(plugin, 'missing') # No error
Attribute Validation¶
# O(n) - one lookup per required name
def validate_object(obj, required_attrs):
"""Return the required names the object lacks - O(n)"""
missing = []
for attr in required_attrs: # n iterations
if not hasattr(obj, attr): # O(1) avg each
missing.append(attr)
return missing if missing else None
class FileWriter:
def write(self):
pass
def close(self):
pass
required = ['write', 'close', 'flush']
missing = validate_object(FileWriter(), required)
# Returns: ['flush'] - missing method
Duck Typing Implementation¶
# Two lookups per capability used: the check and the call
def transform(data):
return data.upper()
def process_data(obj):
"""Work with any object that has the required methods"""
if hasattr(obj, 'read'):
data = obj.read()
else:
raise TypeError("Object doesn't support read")
if hasattr(obj, 'write'):
obj.write(transform(data))
else:
raise TypeError("Object doesn't support write")
# Works with file-like objects
import io
buffer = io.StringIO("content")
process_data(buffer)
print(buffer.getvalue()) # contentCONTENT
Practical Examples¶
Conditional Initialization¶
# Up to three lookups: two checks and one read
class FlexibleConfig:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def get_port(self):
"""Get port with fallback"""
if hasattr(self, 'port'): # O(1) avg
return self.port
elif hasattr(self, 'default_port'): # O(1) avg
return self.default_port
else:
return 8000
config = FlexibleConfig(default_port=9000)
port = config.get_port() # 9000
Plugin System¶
# O(n) - one lookup per feature name
class PluginManager:
def get_capabilities(self, plugin):
"""List plugin capabilities - O(n)"""
capabilities = []
features = ['process', 'validate', 'transform', 'cache']
for feature in features: # n iterations
if hasattr(plugin, feature): # O(1) avg each
capabilities.append(feature)
return capabilities
class SimplePlugin:
def process(self):
pass
def validate(self):
pass
manager = PluginManager()
caps = manager.get_capabilities(SimplePlugin())
# Returns: ['process', 'validate']
Edge Cases¶
Descriptors Run Inside hasattr()¶
# hasattr() finds attributes, methods and properties alike - and runs getters
class Example:
value = 42 # Attribute
def method(self): # Callable
pass
@property
def computed(self): # Property - its getter runs on every hasattr()
print("computing")
return 100
obj = Example()
print(hasattr(obj, 'value')) # True - attribute
print(hasattr(obj, 'method')) # True - callable
print(hasattr(obj, 'computed')) # prints "computing", then True
Side Effects of getattr¶
# __getattr__ runs on every miss - its cost and side effects are hasattr()'s
class WithSideEffect:
def __getattr__(self, name):
print(f"Looking up: {name}")
raise AttributeError(name)
obj = WithSideEffect()
result = hasattr(obj, 'missing') # Prints "Looking up: missing"; result is False
# A __getattr__ that returns instead of raising makes every name exist
class Permissive:
def __getattr__(self, name):
return None
print(hasattr(Permissive(), 'anything')) # True
Best Practices¶
✅ Do:
- Use
hasattr()to test for an attribute before acting on it - Use
getattr()with a default when the value is needed - it is one lookup, not two - Use it for duck typing and protocol checking
- Expect property getters and
__getattr__to run inside it
❌ Avoid:
hasattr()followed bygetattr()on the same namedir()as a way to check for a few names - it builds and sorts the whole attribute list- Relying on
hasattr()to swallow anything butAttributeError - Reassigning a class's attributes in a hot loop on Python 3.13 or later - after more than 1,000 rounds of change and lookup the class is never cached again
Related Functions¶
- getattr() - Get attribute with default
- setattr() - Set attribute value
- delattr() - Delete attribute
- dir() - List all attributes
- vars() - Get dict
- callable() - Check if callable
Version Notes¶
- Python 2.x: Any exception raised by the lookup, not only
AttributeError, is swallowed and reported asFalse - Python 3.2+: Only
AttributeErrorbecomesFalse; every other exception propagates - Python 3.13+: A class that has been changed and looked up again more than 1,000 times stops being cached, so lookups on it are O(d) from then on
- All versions: A non-
strname raisesTypeError