AlgoMaster Logo

Class Attributes

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

A class attribute is a value defined at class scope, outside any method. It's stored on the class itself, not on any individual instance, so every instance of the class sees the same value through the same name. This lesson covers what class attributes are, how Python finds them during attribute reads through an instance, the rules for when an instance "sees" a class attribute versus its own, the shared-mutable-attribute pitfall, and how to decide between a class attribute and an instance attribute when modeling a new class.

Defining a Class Attribute

A class attribute is any name assigned inside the class body, outside a method. The assignment runs once, when the class is being defined, and the result lives on the class object.

tax_rate and currency are class attributes. They aren't set per instance; they belong to Product itself. Every Product instance reads them through the dot, and the value comes from the class:

All three reads return the same value because the value lives in one place: on the Product class. The two instances aren't carrying their own copies; they fall back to the class when they don't have a matching instance attribute. The same fact shows up directly when inspecting the class's __dict__:

The class has its own dict, separate from any instance's __dict__. Class-level assignments (like tax_rate = 0.08) write to the class's dict. Method definitions inside the class body also live there, which is why methods can be called through instances even though they're defined once on the class.

Class attributes that act as constants (values that should never change at runtime) are usually written in UPPER_SNAKE_CASE, like MAX_RETRIES or DEFAULT_TAX_RATE. Class attributes that act as mutable defaults or counters are written in snake_case like instance attributes. Python doesn't enforce either, but the casing signals intent to readers.

The Attribute Lookup Order

Attribute reads check the instance first and fall back to the class. The full lookup spelled out below is the rule that explains every interesting behavior involving class attributes.

When obj.name is read, Python runs through these steps:

  1. Check obj.__dict__ for the name. If found, return it.
  2. Check type(obj).__dict__ for the name. If found, return it.
  3. Walk up the method resolution order (MRO) of the class, checking each base class's __dict__. If found, return it.
  4. If nothing found, raise AttributeError.

For ordinary classes with one parent (object), step 3 is object.__dict__, which carries the built-in dunders. The MRO becomes more interesting with multiple inheritance. For class attributes vs instance attributes, the important steps are 1 and 2: instance first, class second.

The diagram shows the full lookup. For most reads in everyday code, one of the first two checks succeeds. The instance dict is the fast path for attributes that vary per instance (name, price, stock); the class dict is the fast path for shared values and methods (tax_rate, currency, every method defined in the class body).

The asymmetric part to remember: writes always go to the instance. An assignment mouse.tax_rate = 0.05 writes to mouse.__dict__, not to the class. The class attribute is untouched. The instance now has its own tax_rate that shadows the class one for that instance only.

Shadowing: How Instance Attributes Hide Class Attributes

After establishing that reads search the instance first, the next step is what happens when both the instance and the class have an attribute with the same name. The instance wins, but only for that instance. Other instances still see the class value.

Before the assignment, neither instance has a tax_rate of its own, so both reads fall through to the class. The line mouse.tax_rate = 0.05 writes a new key into mouse.__dict__. After that, reading mouse.tax_rate finds the value on the instance and stops (step 1 of the lookup). Reading keyboard.tax_rate still doesn't find anything on the instance and falls through to the class (step 2), so it still returns 0.08. Reading Product.tax_rate directly goes straight to the class and confirms the class value never changed.

The shadowing also appears in the dictionaries:

mouse.__dict__ now has a tax_rate key (the shadow), while keyboard.__dict__ doesn't. The class still has its own tax_rate of 0.08; the lookup stops at the instance for mouse because it found a match there first.

The shadow can be undone with del:

Removing the instance attribute means the lookup falls through to the class again. mouse is back to seeing the shared value.

This shadowing rule is one of Python's stranger corners and the source of plenty of bugs. A short way to keep it straight: reads see the instance if it has the name, otherwise the class. Writes always go to the instance, never the class.

The Shared Mutable Class Attribute Trap

The shadowing rule works as expected for immutable values like strings, numbers, and tuples. Assignments to an instance attribute create a new value on the instance without touching the class. But mutable class attributes (lists, dicts, sets) behave differently.

What's wrong with this code?

Both carts show both items. The class attribute items is one list, and every instance is reading and mutating that one list through self.items. When cart_a.add("Mouse") runs, self.items.append("Mouse") mutates the list living on the class. When cart_b.add("Keyboard") runs, it mutates the same list. The result: every cart sees the same items, because there's only one list.

The trap hinges on the distinction between assignment and mutation:

  • self.items = [...] is an assignment. It creates a new instance attribute that shadows the class one.
  • self.items.append(...) is a mutation. It modifies the object self.items refers to, which (because no instance ever assigned its own items) is the class-level list.

The difference shows up in __dict__:

Neither cart has anything in its own __dict__. The list lives on the class, and both instances are reading and mutating that one list through the fallback lookup.

Fix: initialize the mutable attribute as an instance attribute in __init__, not as a class attribute.

Now each cart gets its own list in __init__, and self.items.append(...) mutates the per-instance list. The class no longer has an items attribute at all; each instance carries its own.

The general rule: mutable values (lists, dicts, sets) should almost always be instance attributes, not class attributes. Class attributes fit shared constants, configuration that genuinely is the same for every instance, and immutable defaults. When the requirement is "every instance should start with an empty list", that list belongs in __init__, not at class scope.

This trap is a correctness issue, not a performance one. The bug usually shows up far from the class definition (a test failing for "no reason", a customer's cart having items from another customer's session), which makes it expensive to debug. The fix takes one line; the diagnosis can take an hour.

The diagram shows the flow of the bug. One list lives on the class. Both instances reach that list through self.items. Each add mutates the same object, so the final state is visible through both instances. The red nodes mark the consequence: the mutation reaches the class, and both carts end up with both items.

Common Uses of Class Attributes

When are class attributes a good fit? A few patterns come up regularly enough to recognize.

Constants

If a value is the same for every instance and shouldn't change at runtime, a class attribute is a clean place for it. The class is the natural namespace, and reading Class.CONSTANT documents that the value belongs to the class.

The three constants describe the class's rules. Storing them on the class means they're available wherever the class is, they're easy to find when looking up the limits, and changing one number updates every place that uses it. Uppercase signals "this is a constant; don't mutate it".

Shared Defaults

If a value is a sensible default for every instance and rarely overridden, a class attribute can serve as the default. Reads fall through to the class until an instance sets its own value, at which point the shadowing rule takes over.

Both orders start with the class defaults (placed, standard). When a updates its status and shipping_method, those updates create instance attributes that shadow the class values for a only. b still reads from the class. This is a clean pattern when most instances genuinely use the defaults and a small minority deviate.

The same pattern would be a bug if the default were mutable (a list, dict, or set), for the reasons covered above. Use class-attribute defaults only for immutable values; for mutable defaults, set them per instance in __init__.

Per-Class Counters

A class attribute that gets mutated through the class itself can serve as a counter or registry. Every instance affects the same shared value, which is sometimes the goal.

The constructor reads and writes Product.instance_count directly (through the class, not through self). Going through the class is the key: self.instance_count = self.instance_count + 1 would read the class value but write to the instance, creating a shadow on every instance and leaving the class counter at 0 forever.

This pattern shows up in factories, ID generators, and bookkeeping for debug tooling. Use it carefully: shared mutable state is easy to misuse, and a function-level counter or a separate registry object is often cleaner.

Class-Level Metadata

Sometimes a class attribute carries metadata about the class itself: a name, a version, a category, a configuration object. It doesn't change per instance and isn't a "default" so much as a property of the class.

The tuple supported_currencies is immutable, so the shared-mutable trap doesn't apply. Reading these through the class (without creating any instance) is the typical access pattern, because the metadata describes the class itself, not any particular instance.

Class vs Instance: How to Choose

When modeling a new class, the question "should this attribute be on the class or on the instance?" comes up for every piece of data. A few rules of thumb:

QuestionIf the answer is yes...
Will this value differ from one instance to the next?Instance attribute (set in __init__)
Is this value mutable (list, dict, set)?Instance attribute (almost always)
Is this value the same for every instance and shouldn't change at runtime?Class attribute (constant, uppercase)
Is this value an immutable default that most instances use but a few override?Class attribute (snake_case, but consider explicit instance default for clarity)
Are you tracking something across the whole class (counter, registry, cache)?Class attribute, mutated through the class
Does this represent the class's identity or metadata (name, version)?Class attribute

The single most reliable instinct: if it's mutable and per-instance, use `__init__`. That one rule sidesteps the worst of the shared-mutable trap. When in doubt about an immutable value, defaulting to an instance attribute is usually safe; an immutable value can be promoted to a class attribute later if it really is the same across every instance.

A worked example that uses both:

Every choice in the class fits its category: MAX_DISCOUNT and DEFAULT_CURRENCY are immutable and shared (class), instance_count is shared mutable state managed through the class (class, with explicit Product.attr updates), and name, price, currency, and tags vary per instance (set in __init__). The mutable tags list is per instance, which sidesteps the shared-list trap.

Type Annotations on Class Attributes (PEP 526)

Python allows annotating class attributes with type hints, the same way function parameters are annotated. The annotations don't change runtime behavior; they document intent and help tools like mypy catch type mistakes.

The : float and : str annotations are documentation as far as the interpreter is concerned. Reading them shows a future reader the expected type of each attribute, and type checkers can flag code that assigns the wrong type.

A ClassVar annotation also exists for cases where being explicit that an attribute is a class attribute, not an instance attribute, matters. This is most important with @dataclass, which would otherwise treat any annotated class-level assignment as an instance attribute description:

The ClassVar[float] annotation tells @dataclass "don't include this in the generated __init__; keep it on the class". Without ClassVar, the dataclass would add DEFAULT_TAX_RATE to the constructor's parameter list, which is almost never the right behavior for a constant. The print at the end confirms the attribute didn't end up on the instance.

ClassVar can be used outside dataclasses too, where it's purely documentation. Tools and readers see "this is intentionally a class attribute"; the interpreter still resolves the lookup through the same MRO walk as any other class-level name.

Quiz

Class Attributes Quiz

10 quizzes