Python Object Namespace: How Attribute Lookup Works
python object namespace: Understand how Python object namespaces work: instance __dict__, class attribute lookup, __slots__, and programmatic namespace manipulation.
When you create an instance of a class, Python does not store attributes in a single flat table. Each object carries its own namespace, and the class contributes another namespace. Attribute access resolves through both. This is the core of the python object namespace model, and understanding it explains why some attribute assignments behave unexpectedly.
class Order: def __init__(self, sku, quantity): self.sku = sku self.quantity = quantity def total(self): return self.quantity * 10 order = Order("A-100", 3) print(order.__dict__)
The instance namespace lives in order.__dict__, a dictionary that maps attribute names to values. When you access order.sku, Python looks in order.__dict__ first, finds "sku", and returns "A-100". The class's namespace, Order.__dict__, holds the method total and other class-level definitions. The instance namespace and the class namespace are separate dictionaries, and the lookup path between them defines how attributes resolve.
Instance Namespace: The __dict__ Dictionary
Every normal instance has a __dict__ attribute. It is a plain dictionary, which means attribute storage is dynamic. You can add attributes that were never declared in the class:
order.discount = 0.15 print(order.__dict__)
This dynamic behavior is what makes Python flexible, but it also creates a maintainability concern. A typo in an attribute name silently creates a new namespace entry instead of raising an error. Writing order.quanity = 5 does not fail; it just adds a separate entry that no other code reads. The same problem appears when a library expects a specific attribute and receives a misspelled one.
The __dict__ dictionary is also directly inspectable. vars(order) returns the same dictionary object, which is convenient for debugging or serialization. However, relying on __dict__ in production code couples you to an implementation detail that does not exist on every object type.
Class Namespace and Attribute Lookup Order
When an attribute is not found in the instance __dict__, Python moves to the class namespace, then through the method resolution order (MRO) for inheritance. The lookup order is: instance __dict__ first, then the class __dict__, then base classes in MRO order, and finally the __getattr__ hook if it is defined.
class Order: tax_rate = 0.08 class DiscountedOrder(Order): tax_rate = 0.05 order = DiscountedOrder("A-100", 3) print(order.tax_rate)
order.tax_rate finds tax_rate in DiscountedOrder.__dict__ before checking Order. Class attributes are shared across all instances. If you mutate a mutable class attribute, every instance observes the change unless an instance shadows it with its own entry in __dict__.
This shared behavior is a common source of bugs. A list or dictionary assigned at class level is created once, not per instance. Assigning it in __init__ moves the attribute into each instance namespace, which is what most code actually wants.
__slots__: Replacing the Instance Namespace
__slots__ removes the instance __dict__ and replaces it with a fixed set of descriptors. Each instance no longer carries a dictionary, which reduces memory usage, and accidental attribute creation is prevented.
class Order: __slots__ = ("sku", "quantity") def __init__(self, sku, quantity): self.sku = sku self.quantity = quantity order = Order("A-100", 3) print(order.sku)
With __slots__, order.discount = 0.15 raises AttributeError because the namespace is fixed. The tradeoff is reduced flexibility: you must declare every instance attribute in advance, and you lose the ability to attach arbitrary data to an instance. This is appropriate for high-volume objects where memory matters, such as records processed in large batches.
There is a subtle inheritance rule. A subclass that does not define __slots__ still gets a __dict__, so the memory savings disappear unless every class in the hierarchy declares __slots__. Also, __slots__ does not remove the class namespace; class attributes and methods still live there as usual.
Manipulating Namespaces Programmatically
setattr, getattr, and delattr operate on namespaces without hardcoding attribute names in the source. This is useful when attribute names come from configuration, user input, or a data-driven mapping.
field = "sku" value = "B-200" setattr(order, field, value) print(getattr(order, field)) delattr(order, field)
getattr raises AttributeError when the attribute is missing. Provide a default argument or catch the exception when the attribute may not exist:
sku = getattr(order, "sku", None)
These functions work on the same lookup path as normal attribute access. setattr writes to the instance namespace when the attribute is not a descriptor on the class, while getattr follows the full lookup order. Understanding the namespace model makes the behavior of these functions predictable.
Namespace and Maintainability Concerns
The dynamic namespace is the main source of subtle bugs in Python codebases. A misspelled attribute in one module silently creates a new namespace entry, and the error only surfaces later when the value is read. Code that relies on vars() or __dict__ breaks when __slots__ is introduced, because those objects no longer have a __dict__.
For maintainable code, treat the instance namespace as a defined contract. Use __slots__ when the set of attributes is stable and known in advance. Document when dynamic attributes are intentionally allowed, for example in data-transfer objects that accept arbitrary fields. Avoid depending on __dict__ in library code, because the implementation detail is not guaranteed across all object types, including built-ins and extension types.
When refactoring a class to use __slots__, search for any code that attaches ad-hoc attributes to instances. That code will fail at runtime with AttributeError. The failure is immediate and clear, which is preferable to the silent acceptance of typos, but it still requires a deliberate migration.
Common Mistakes with Object Namespaces
Two frequent errors stand out. First, assigning a mutable class attribute and expecting per-instance isolation:
class Order: items = [] order_a = Order() order_b = Order() order_a.items.append("sku-1") print(order_b.items)
Both instances share the same list because items lives in the class namespace. To get per-instance storage, assign in __init__:
class Order: def __init__(self): self.items = []
Second, using __slots__ without accounting for inheritance. A subclass that omits __slots__ still receives a __dict__, so the memory savings are lost. Every class in the hierarchy must declare __slots__ for the optimization to hold across the entire object graph. This is a deliberate design decision, not an oversight in the language, and it rewards planning the attribute set before writing the class hierarchy.