DSM
60 XP
25 minsIntermediate60 XP

Attributes & Methods in Depth

Two sensors share one readings list and nobody knows why. A 'constant' changed on one instance and stayed the same on the class. A method mutates state AND returns a value, and callers keep double-applying it. All three bugs come from the same gap: attributes have RULES you haven't been told yet.

What you'll learn

  • Distinguish class attributes from instance attributes
  • Trace the instance → class attribute lookup
  • Avoid (and diagnose) the shared-mutable class attribute trap
  • Design methods as commands (mutate) or queries (return) — not both
  • Use class attributes deliberately: constants, defaults, counters

What

The two attribute layers — class attributes (shared, on the blueprint) and instance attributes (per-object, set via self) — plus how lookup falls back from instance to class, and the method-design habits (commands vs queries) that keep objects predictable.

Why

The class-vs-instance distinction causes real production bugs (the shared mutable default is a classic), and method design is API design: every library you'll use made these choices, and every class you write makes them for your teammates.

Where it's used

Defaults and constants on classes, per-object state in __init__, counters shared across instances, and every 'should this method return or mutate?' decision.

Where this runs in production

DjangoModels declare with class attributes

A Django model lists its fields as class attributes — the framework reads the blueprint to build database tables, while each row instance carries its own values.

sklearnConfig vs learned state

Estimator hyperparameters arrive in __init__ (instance attributes); the trailing-underscore convention (coef_) marks state that fit() attached later — an attribute-layer discipline you can read.

StripeCommand-query separation in SDKs

Well-designed client libraries keep mutations (charge.capture()) separate from queries (charge.status) — the discipline this lesson names, at API scale.

Theory

The core ideas, in plain language.

Attributes assigned in the CLASS BODY belong to the class itself — one copy, shared by every instance. Attributes assigned through SELF (usually in __init__) belong to each instance — one copy per object. Both are read with the same dot syntax, which is exactly why the distinction hides.
class Sensor:
    unit = 'celsius'          # class attribute: shared

    def __init__(self, sid):
        self.sid = sid        # instance attribute: per-object

a = Sensor('T1')
b = Sensor('T2')
print(a.unit, b.unit)        # both read the ONE class copy
print(a.sid, b.sid)          # each reads its own

a.unit finds nothing on the instance, so lookup FALLS BACK to the class — that fallback is the mechanism behind everything in this lesson. Methods themselves are class attributes too; that's why one definition serves all instances.

Analogy: The gym's rulebook and the members' lockers

A gym has ONE rulebook on the wall (class attributes) and a locker per member (instance attributes). Ask any member 'what are the opening hours?' and they check the wall — one source, everyone agrees. Ask 'where are your shoes?' and they open THEIR locker. The trap: if the wall holds a shared whiteboard ('equipment wishlist') and every member scribbles on it thinking it's their private list, chaos — that's the mutable class attribute bug. And if one member tapes their own hours over the wall poster, THEY see their version while everyone else still sees the wall — that's instance shadowing.

Key Concept
Assignment through self SHADOWS, never updates, the class

Reading falls back instance → class, but WRITING through self always writes to the instance. a.unit = 'kelvin' doesn't change the class's unit — it creates an instance attribute that shadows it for `a` only. Same word as the scope lesson, same behavior: the outer name is hidden, not modified. To change the shared value for everyone: Sensor.unit = 'kelvin' — write where the attribute lives.

Watch out
THE trap: mutable class attributes

class Sensor: readings = [] puts ONE list on the class. Every instance's self.readings.append(...) finds that shared list via fallback — because append MUTATES (no assignment, no shadowing!) — and all sensors' data silently merges. It's the mutable-default-argument bug wearing a class costume, and the fix is identical: mutable per-instance state is created in __init__ (self.readings = []), where each construction builds a fresh object. Rule: class attributes are for IMMUTABLE constants and defaults; anything mutable goes through __init__.

Key Concept
Commands and queries: pick one per method

A COMMAND mutates state and returns None (or self for chaining): account.deposit(50), list.append(x) — note append returning None is deliberate design, and why x = lst.append(3) is a classic bug. A QUERY computes and returns without mutating: account.balance_in('EUR'), df.describe(). Methods that do both — mutate AND return the value — invite double-application bugs and untestable call sites. Name commands with verbs, queries with nouns or get_/is_ prefixes, and readers can predict behavior from signatures.

Inspection tools when attribute layers confuse you: vars(obj) shows ONLY the instance's own attributes (its __dict__); vars(ClassName) shows the class's. If a.unit prints a value that vars(a) doesn't contain, you're watching the fallback in action. These two calls turn every attribute mystery into a two-line diagnosis.
Visual Learning

See the concept, then explore it.

Attribute lookup: instance first, class as fallback

What happens on a.unit — and why writes behave differently. Click each node.

Worked Examples

Watch it built up, one line at a time.

Very EasyShared constant, per-instance data

All invoices share a tax rate; each has its own amount.

Step 1 of 1

TAX_RATE lives once, on the class (CAPS names the constant, as at module level); each invoice's amount is its own. self.TAX_RATE reads via fallback — instance has none, class provides.

Code
01class Invoice:
02 TAX_RATE = 0.08 # shared constant
03 
04 def __init__(self, amount):
05 self.amount = amount # per-instance
06 
07 def total(self):
08 return round(self.amount * (1 + self.TAX_RATE), 2)
09 
10print(Invoice(100.0).total())
11print(Invoice(59.99).total())
Output
108.0
64.79
Practice Coding

Your turn — write the code.

Your task

Build an ApiClient class: a class attribute BASE_URL = 'https://api.acme.io' shared by all clients; per-instance token and a request LOG list (created safely in __init__); a command get(path) that appends f'GET {BASE_URL}{path}' to the log; and queries request_count() and last_request(). Prove two clients keep separate logs.

Expected output
2 1
GET https://api.acme.io/orders
GET https://api.acme.io/health

Write your solution in the editor on the right, then hit Run.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

Where should a per-instance mutable list be created?

class Config: mode = 'fast' a = Config(); a.mode = 'safe' What is Config.mode now?

Medium0/3 solved

Why does the shared-mutable bug involve .append() but NOT plain assignment (self.x = ...)?

ScenarioA teammate's method: def apply_discount(self, pct): self.price *= (1 - pct/100); return self.price. Callers write total += cart.apply_discount(10) in three places — and a bug report says some carts get discounted twice.

What design change prevents this class of bug?

Define Robot with a class attribute fleet = 'R2' and per-instance name. Create r1 and r2; set r2.fleet = 'X9'; print r1.fleet, r2.fleet, Robot.fleet. Expected output: R2 X9 R2

Shadowing demonstrated:r2 carries its own fleet; r1 and the class keep 'R2'
Layers correct:fleet in class body; name via self in __init__
Hard0/2 solved

A counter: class C: n = 0, and __init__ does self.n += 1 intending to count instances. After C(); C(); C(), what is C.n?

Build TicketSystem: class attribute issued = 0 counting ALL tickets ever created across instances; __init__(self, desk) sets the desk name; command issue() increments the class counter AND a per-instance count (created in __init__); query stats() returns '<desk>: <mine> of <total>'. Create desks a and b; issue 2 on a, 1 on b; print both stats. Expected: north: 2 of 3 south: 1 of 3

Class counter via class name:issue writes TicketSystem.issued += 1, not self.issued
Two layers coexist:Per-desk mine and shared issued track independently
Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

Explain class attributes vs instance attributes, including the lookup and write rules.

Show model answer

Class attributes are assigned in the class body and stored once on the class object; instance attributes are assigned through self (typically in __init__) and stored per object. READS use fallback: Python checks the instance's own __dict__ first, then the class (then base classes) — so instances 'see' class attributes they never defined, and methods themselves are just class attributes found by this same lookup. WRITES never climb: obj.x = v always lands on the instance, creating a shadow that hides the class copy for that object alone; changing the shared value requires writing through the class (C.x = v). The asymmetry — reads fall back, writes don't — explains both deliberate patterns (per-instance overrides of class defaults) and the famous trap: augmented assignment self.n += 1 reads the class value then writes an instance shadow, silently breaking shared counters.

Describe the shared-mutable class attribute bug and its relationship to the mutable default argument.

Show model answer

Declaring class Sensor: readings = [] creates ONE list on the class. Instance code doing self.readings.append(v) reads 'readings' — fallback finds the class's list — and mutates it in place. No assignment occurs, so no shadowing rescues you: every instance appends to the same list, and objects that should be independent share state. The symptom is cross-contamination ('sensor A has sensor B's data'); the diagnosis is a.readings is b.readings returning True. It's the same disease as the mutable default argument (def f(x, acc=[])): in both, a mutable object is created ONCE at definition time and then shared across uses that each believe it's theirs. Same cure, too: create mutable state at USE time — in __init__ for classes, inside the body for functions. The safe class-body residents are immutable: numbers, strings, tuples, None.

What is command-query separation and why does list.append returning None embody it?

Show model answer

CQS is the design rule that a method should either be a COMMAND — mutate state, return nothing meaningful — or a QUERY — return information, mutate nothing — never both. Queries become safe to call anywhere (logs, tests, f-strings, repeated reads) because they can't change behavior; commands become obviously effectful, and calling one twice visibly does the thing twice. Mixed methods breed real bugs: apply_discount() that mutates AND returns the price gets called wherever the price is needed, discounting repeatedly. list.append returning None is the canon example: Python could return the list, but then x = lst.append(3) would look reasonable while hiding a mutation, and chained appends would obscure state changes — instead the None forces you to notice it's a command. In my own classes I signal the split by naming: verbs for commands (add, clear, issue), nouns/get_/is_ for queries (subtotal, count, is_empty) — the signature then documents the side-effect contract for free.

Common Mistakes to Avoid

1) Mutable class attributes (lists/dicts in the class body) — one shared object; create them in __init__. 2) self.counter += 1 for a shared counter — reads class, writes instance shadow; use ClassName.counter += 1. 3) Expecting obj.x = v to update the class value — writes never climb. 4) Methods that mutate AND return the mutated value — double-application bait. 5) Constants scattered as magic numbers in methods instead of named class attributes. 6) Debugging attribute mysteries by guesswork — vars(obj) and vars(Class) show exactly which layer holds what.

Ask the AI Tutor

Try these prompts in the AI Tutor panel: • 'Quiz me: for each of these six attribute accesses, which layer answers?' • 'Walk me through the shared-mutable trap with is-checks.' • 'Show the self.n += 1 counter bug step by step.' • 'Audit this class: which methods violate command-query separation?' • 'Interview mode: ask me the read/write asymmetry and grade my answer.'

Glossary

Class attribute — assigned in the class body; one shared copy. Instance attribute — assigned through self; per-object. Fallback lookup — reads check instance first, then class. Shadowing — an instance attribute hiding a same-named class attribute. Shared-mutable trap — a mutable class attribute silently shared by all instances. Command — a method that mutates and returns None/self. Query — a method that returns without mutating. CQS — command-query separation. vars(x) — x's own attribute dict (instance or class). Augmented assignment — read-then-assign (+=), which shadows through self.

Recommended Resources

• Docs: 'Class and Instance Variables' in the official tutorial's classes chapter — the tutorial's own dog-tricks example IS the shared-mutable trap. • Read: Martin Fowler's short 'CommandQuerySeparation' note — two minutes, permanent vocabulary. • Practice: write a class with a deliberate class-level counter and a per-instance list, then prove each layer with vars() before trusting it. • Next in DSM: classes can BUILD ON other classes — Inheritance lets a specialized type reuse and override a general one, and it's how every sklearn estimator plugs into the same API.

Recap

✓ Class body → shared attribute; __init__ via self → per-instance attribute. ✓ Reads fall back instance → class; writes always land on the instance (shadowing). ✓ Mutable state belongs in __init__ — the class-body list is the shared-mutable trap. ✓ Shared counters write through the class name; self.n += 1 silently forks. ✓ Commands mutate and return None; queries return and mutate nothing — never both. ✓ vars(obj) / vars(Class) turn attribute mysteries into two-line diagnoses. Next up: Inheritance. One class can extend another — reusing its attributes and methods, overriding what differs — the mechanism behind every plugin system and sklearn's uniform estimator API.

scratchpad — preview this lesson's challenge anytime
api_client.pyPython
Ready
Output

Run your code to see the output here.