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
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
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.
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.
Well-designed client libraries keep mutations (charge.capture()) separate from queries (charge.status) — the discipline this lesson names, at API scale.
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 owna.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.
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.
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.
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__.
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.
What happens on a.unit — and why writes behave differently. Click each node.
All invoices share a tax rate; each has its own amount.
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.
108.0 64.79
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.
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.
Where should a per-instance mutable list be created?
class Config: mode = 'fast' a = Config(); a.mode = 'safe' What is Config.mode now?
Why does the shared-mutable bug involve .append() but NOT plain assignment (self.x = ...)?
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
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
Explain class attributes vs instance attributes, including the lookup and write rules.
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.
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?
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.
Run your code to see the output here.