AlgoMaster Logo

Instance Attributes

High Priority23 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

An instance attribute is a piece of data attached to one specific object, not to the class as a whole. Writing self.name = name inside __init__ creates an instance attribute on the new object: it lives on that one instance, and other instances of the same class have their own separate copies. This lesson covers how instance attributes are stored, how to read, set, and delete them, the tools Python provides for inspecting them, and the small set of pitfalls that show up when classes carry state.

Where Instance Attributes Live

Every regular Python instance carries a hidden dictionary called __dict__. Writing self.price = 29.99 adds the key "price" with the value 29.99 to that dictionary. Reading instance.price later looks the key up. The dot in instance.price is shorthand for a dictionary lookup with a few extra rules.

A class small enough to see the storage directly:

Two attributes, two keys in the dict. Every assignment to self.something inside the constructor (or anywhere else) writes into that dict. Reading mouse.name is roughly mouse.__dict__["name"] plus the extra lookup machinery Python uses to fall back to the class when an attribute isn't on the instance.

The point of __dict__ is that each instance has its own. Two Product objects have two separate __dict__ dictionaries, so writing to one doesn't show up in the other:

Two instances, two __dict__ dictionaries, two separate copies of the attribute storage. This is what makes object-oriented code work: each instance carries its own state, isolated from every other instance of the same class.

Most Python classes have __dict__, but not all. Classes that define __slots__ use a more compact storage that's faster and uses less memory but doesn't allow arbitrary new attributes after construction. __slots__ is covered at the end of this lesson; for most Python code, assume __dict__ is present.

Setting Instance Attributes Inside __init__

The standard place to set instance attributes is inside __init__. The constructor receives the values from the caller and writes them onto the new instance. The reason this is the standard pattern is worth spelling out.

Putting the attribute setup in __init__ has two practical benefits. First, every instance starts life with the same shape: every Product has a name, a price, and a stock. Code that uses Product instances doesn't have to defensively check whether each attribute exists, because the constructor guarantees they do. Second, the __init__ body is the canonical place to look up "what attributes does this class carry?" A future reader opens the class, reads the constructor, and learns the shape of the data.

Attributes can also be computed inside __init__ rather than just stored from parameters:

subtotal and total aren't constructor parameters; they're derived from items and tax_rate inside the body. The caller doesn't compute them, and the values are then available as plain attributes on the instance. The trade-off is that they're a snapshot from construction time: if the caller mutates cart.items later, cart.subtotal won't update on its own. Keeping derived values in sync with their sources is what @property is for.

Setting Attributes Outside __init__

Python is permissive about attribute creation. Attributes can be added to an instance from outside the class, after construction, by assigning to a name through the dot:

After the assignments, mouse has four attributes. category and tags weren't mentioned in __init__, but the assignment created them anyway. This dynamic behavior is sometimes useful (decorating an instance with extra data for a specific use case, attaching debug info) but it's discouraged for the everyday case of "this class has these attributes". The problem is that the shape of the data becomes inconsistent: some Product instances have a category and some don't, depending on whether one was set after construction. Code that reads product.category has to handle the missing case.

The conventional rule: declare all the attributes a class carries in `__init__`, even if they start as None or 0 or an empty list. That way every instance has the same shape, and the missing-attribute case never arises.

Now every Product has all four attributes from the moment it's created, even when the caller omits category and tags. The shape is consistent, and reading mouse.category always returns something (possibly None), never raises.

Setting new attributes from outside the class is fine for quick experiments at the REPL or for one-off scripts. For code that will be revisited, keep the attribute set inside __init__.

Reading and Mutating Instance Attributes

Reading an attribute uses the same dot syntax as setting one. Python looks the name up, returns the value, and the surrounding code uses it.

Each read goes through the same lookup. The attribute names work in any context that accepts a value: f-strings, arithmetic, comparisons, function arguments.

Mutating an attribute means assigning a new value to the same name. The previous value is replaced:

The assignment mouse.price = 24.99 updates the "price" key in mouse.__dict__. There's nothing special about modifying an attribute after construction; it's the same operation as the initial assignment in __init__. The choice of when to allow mutation is a design question, not a language one. Some classes treat their attributes as fixed once __init__ finishes (immutable-style); others mutate freely. Python doesn't enforce either, but conventions like @property and @dataclass(frozen=True) exist for stricter rules.

Methods that mutate state typically read and write attributes through self:

restock reads self.stock, adds the quantity, and writes the result back. sell does the inverse with a validation check. Both work on whichever instance they were called on, because self is that instance.

Deleting Instance Attributes

An attribute can be removed from an instance with del. After deletion, reading the attribute raises AttributeError (or falls back to the class, if a class attribute with the same name exists).

The del statement removes the "category" key from mouse.__dict__. Reading mouse.category afterwards fails because the key is gone, and there's no class-level category attribute to fall back to.

Deleting attributes is rare in everyday code. It comes up occasionally when an attribute should only exist during a specific phase of an object's life (a temporary cache to clear, or a one-time setup value), but for normal classes, attributes are set in __init__ and left there, possibly with their values mutated. Use del obj.attr only when there's a real reason to remove the attribute entirely rather than reset it.

Inspecting Instance Attributes

Python provides a few tools to ask "what attributes does this instance carry?" The most direct is __dict__, which returns the storage dictionary itself:

vars(obj) is the same thing through a built-in function:

vars(obj) and obj.__dict__ return the same dict. The function form is sometimes a little clearer in code that uses it programmatically, and it works on modules too (vars(some_module) returns the module's globals). For inspecting an instance, both are interchangeable.

dir(obj) returns a longer list of names, including methods inherited from the class and object:

dir(obj) lists everything accessible through the dot, including methods. The filter above strips out the dunder-prefixed names (like __init__, __class__, __repr__) so only the user-defined names appear. For "what attributes does this specific instance carry?", use vars(obj) or obj.__dict__. For "what can be done with this object, including inherited methods?", use dir(obj).

A single attribute can also be checked with hasattr and read with a default through getattr:

hasattr(obj, "attr") returns True if the attribute exists (on the instance or its class). getattr(obj, "attr") returns the value, and getattr(obj, "attr", default) returns the default when the attribute is missing instead of raising. These pair well in code that handles optional attributes without throwing.

hasattr is implemented by trying getattr and catching AttributeError. For attributes that exist, it's fast. For ones that don't, it pays the cost of raising and catching an exception. In tight loops, comparing against a known set or using getattr(obj, "attr", None) is usually cheaper than two separate hasattr plus getattr calls.

How Lookup Sees the Instance Dict

When mouse.name is read, Python doesn't go straight to mouse.__dict__["name"]. It runs through a lookup procedure that checks several places before giving up. Knowing the order helps make sense of behavior that otherwise looks strange. The simplified version is:

  1. Check the instance's __dict__ for the name.
  2. If not found, check the class's __dict__ (and its base classes).
  3. If still not found, raise AttributeError.

For purely instance attributes (the ones set with self.x = ...), step 1 finds them and the lookup stops:

The diagram shows the simple path: a read for an attribute that lives on the instance finds it in step 1 and returns immediately. The instance's storage is checked first, and the class isn't consulted unless the instance doesn't have the name.

The same lookup applies to writes, but with one important difference: writes always go to the instance. mouse.name = "Updated" writes to mouse.__dict__["name"], never to the class. This is why mutating an attribute on one instance doesn't affect any other instance: each instance has its own __dict__, and writes never reach across.

This split (reads check both instance and class, writes only touch the instance) is the source of one of the common pitfalls with classes, the shared-mutable-class-attribute trap. The key point is that the asymmetry exists.

Dynamic Attribute Creation

Python's permissiveness about adding attributes after the fact has one occasionally useful corollary: an instance's attributes can be built from a dictionary or a sequence of pairs at runtime.

setattr(obj, "name", value) is the function form of obj.name = value. The pair (setattr and getattr) reads and writes attributes when the attribute name is itself a variable. This pattern shows up in code that builds objects from external data (rows from a CSV, fields from a JSON payload) without writing each attribute by hand.

The drawback is the same as setting attributes outside __init__: the shape of the object isn't documented anywhere. A future reader doesn't know what attributes a Product carries without running the program. For everyday classes, declare the attributes explicitly in __init__. For genuine "building an object from a dynamic schema" cases, the **kwargs plus setattr pattern fits.

A more structured alternative for the same use case is types.SimpleNamespace, a built-in throwaway class designed for dot-accessible attribute bags:

SimpleNamespace is "an object whose attributes are whatever the constructor receives, with a useful __repr__ and equality." It's not a replacement for class, but it's handy for quick scripts or for converting a dict into a dotted-access object.

Common Pitfalls

Some pitfalls show up with instance attributes often enough to deserve direct attention.

Mutable Defaults Inside __init__

If a constructor parameter has a mutable default, every instance that uses the default ends up sharing one object:

What's wrong with this code?

The default [] is evaluated once when the def runs, and that one list is reused as the default for every call. cart_a.items and cart_b.items end up referring to the same list. The fix is the None sentinel pattern:

Fix:

Now each call that omits items gets a fresh empty list. The trap applies to lists, dicts, sets, and any other mutable default; immutable defaults (None, 0, "", ()) are fine.

Shadowing Class Attributes by Accident

If a class has an attribute (a shared default, a constant), and an assignment to the same name happens through self, the result is an instance attribute that shadows the class one. The class attribute still exists; the instance just stops seeing it:

The third line reads from the class. The fourth line reads from the instance, which now has its own tax_rate shadowing the class's. The fifth line shows the class attribute is unchanged. This isn't necessarily a bug (overriding a default for one instance can be intentional), but it's a common source of confusion when an instance "looks like" it's set a shared value and the next instance doesn't see the change.

Typos Create New Attributes Instead of Erroring

Python doesn't enforce a fixed set of attributes per class by default. A mistyped attribute name on the left side of an assignment doesn't raise an error; it creates a new attribute with the wrong name:

The intent was to update price, but the typo prce created a new attribute. The original price is still 29.99. This kind of bug is easy to introduce and frustrating to find, because the code runs without an error; it does the wrong thing without complaint.

Tools that help: type checkers like mypy warn about unknown attribute names when the class's attributes are declared (with annotations or dataclasses). Editors with Python language support catch many of these in real time. For classes where runtime enforcement matters, defining __slots__ (covered briefly below) makes typos raise AttributeError immediately.

dataclasses for Boilerplate-Free Instance Attributes

A common pattern in Python classes is "constructor that takes a bunch of values and stores them as instance attributes, plus a sensible __repr__ and maybe equality". The dataclasses module, added in Python 3.7, generates that boilerplate from a list of type-annotated attributes. They're worth introducing here because they directly solve the "writing the same __init__ over and over" problem.

The @dataclass decorator reads the annotated attributes (name, price, stock) and writes an __init__ that takes them as parameters and stores them as instance attributes. The default value on stock becomes the default in the generated constructor. A useful __repr__ and equality comparison are generated as well.

Dataclasses don't replace classes for everything. They fit when the class is mostly a structured bundle of data with a few methods. For substantial logic in the constructor (validation, derived values, complex setup), a hand-written __init__ fits better.

@dataclass runs at class-definition time, generating methods once when the class is loaded. There's no per-instance overhead at runtime; instances behave like instances of a hand-written class.

A Quick Note on __slots__

Most Python classes store attributes in __dict__. That allows flexibility (any attribute can be added at any time) but uses a few hundred bytes per instance for the dict's overhead. For classes that will create millions of instances and have a fixed attribute set, __slots__ is an alternative storage mechanism:

With __slots__ declared, the class uses a fixed array of slots instead of a __dict__. Instances are smaller (no dict overhead) and attribute access is slightly faster. The trade-off is rigidity: only the names listed in __slots__ can be set, so typos and dynamic additions raise AttributeError. That can be a feature (early failure on typos) or a constraint (no flexibility for extensions), depending on the use case.

It's good to know __slots__ exists. For most code, plain classes with __dict__ are the right choice. Use __slots__ only when memory or attribute-typo enforcement is a measured concern.

Quiz

Instance Attributes Quiz

10 quizzes