You've been using objects since day one: 'hello'.upper(), my_list.append(), and soon df.groupby(). Every one is DATA carrying its own BEHAVIOR. Today you stop just using such types and start defining them — because some state you've been threading through function after function deserves a home of its own.
What you'll learn
What
A class is a blueprint for a new type: what data its instances hold (attributes) and what they can do (methods). An object is one instance built from that blueprint. __init__ sets up each new instance; self is how methods reach the instance they belong to.
Why
When several functions all take the same bundle of state (account, balance, history...), the design is begging for a class. And every data science library is classes: DataFrame, LinearRegression, Figure — defining your own makes their APIs transparent instead of magical.
Where it's used
Modeling domain entities (accounts, sensors, experiments), pipeline components, sklearn-style estimators, and reading ANY library's source code.
Where this runs in production
LinearRegression() constructs an instance; .fit() mutates its state (learned coefficients); .predict() uses that state. The whole estimator API is this lesson.
pd.DataFrame(data) calls a constructor; df.shape reads an attribute; df.groupby() calls a method. Three syntaxes you'll now recognize as one mechanism.
Pricing pipelines model a Listing as a class — attributes for location and amenities, methods for fee calculation — so business rules live WITH the data they govern.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
acct = BankAccount('Ada', 100.0)
print(acct.owner) # Ada
print(acct.balance) # 100.0Calling the CLASS like a function builds an instance: Python creates a blank object, passes it to __init__ as self, your assignments attach the data, and the finished object comes back. acct.owner reads an attribute with dot notation — like a dict field, but with a fixed, documented shape.
A class is an architect's blueprint; objects are the houses built from it. One blueprint, many houses — each with its own address, paint color, and furniture (attribute VALUES differ), all sharing the same layout (the same attributes and methods EXIST). Renovating one house doesn't touch the others — but changing the blueprint changes every house built after. And 'self' is just the builder's phrase for 'THIS house, the one we're standing in'.
Every method's first parameter is self — the instance the method was called ON. acct.deposit(50) is sugar for BankAccount.deposit(acct, 50): Python passes acct as self for you. That's the entire mystery. Inside the method, self.balance is 'THIS account's balance' — which is how two accounts keep separate balances while sharing one method definition.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
a = BankAccount('Ada', 100.0)
b = BankAccount('Kai', 50.0)
a.deposit(25.0)
print(a.balance, b.balance) # 125.0 50.0One deposit method, two independent balances — self routes each call to the right instance's data. This is the closure-counter from Scope & Closures with an explicit, inspectable home for its state.
Reach for a class when: several functions all pass around the same bundle of state; the state must stay CONSISTENT under defined operations (a balance only changes via deposit/withdraw); or you're building many instances of one shape with behavior. Stick with dicts + functions when it's pure data passing through transformations — most ETL rows never need to be objects. The smell that demands a class: def deposit(account_dict, amount), def withdraw(account_dict, amount)... every function taking the same first argument.
1) Forgetting self in a method signature — 'takes 1 positional argument but 2 were given' means Python passed the instance and your def had no slot for it. 2) Writing balance instead of self.balance inside a method — that's a local variable that vanishes when the method returns (the Scope lesson, striking again). 3) BankAccount() vs BankAccount — with parentheses you get an INSTANCE; without, the class object itself (the function-reference distinction, one level up).
Construction, step by step. Click each stage.
Model a temperature sensor with an ID and a unit.
The full lifecycle in six lines: blueprint, construction, attribute access. Note __init__'s parameters become the constructor's signature.
T-101 celsius
Your task
Build a Playlist class: __init__ takes a name and starts an empty track list; add(title, minutes) appends a (title, minutes) tuple; total_minutes() returns the summed duration; describe() returns '<name>: <n> tracks, <total> min'. Create one playlist, add three tracks, print describe().
Focus: 3 tracks, 14.0 min
Write your solution in the editor on the right, then hit Run.
What is the relationship between a class and an object?
In acct.deposit(50), what does the method receive as self?
Inside a method you write `count = count + 1` intending to update the instance's counter, and get UnboundLocalError. Why?
What does this design want to become?
Define a Counter class: __init__ starts count at 0; increment() adds 1; report() returns 'count: N'. Create one, increment three times, print report(). Expected output: count: 3
a = Sensor('T1'); b = Sensor('T1') — is a is b True, and what about a separate c = a?
Define Inventory: __init__ takes a name; add(item, qty) accumulates quantities in a dict (merging repeat items); missing(required) takes a set of item names and returns the sorted list of those not stocked. Create one, add ('bolt', 5), ('nut', 2), ('bolt', 3), then print the stock dict and missing({'bolt', 'washer', 'screw'}). Expected: {'bolt': 8, 'nut': 2} ['screw', 'washer']
What is self, mechanically? Why do Python methods need it?
self is the instance a method was invoked on, received as the method's first parameter. The dot syntax is sugar: acct.deposit(50) is exactly BankAccount.deposit(acct, 50) — Python looks deposit up on the class, then calls it with the instance prepended. This is why one method definition serves every instance: the shared code reaches per-instance data only through self, so a.deposit(25) and b.deposit(25) touch different balances. It also explains the two classic errors — omitting self in the def gives 'takes 1 positional argument but 2 were given' (the auto-passed instance had no slot), and writing balance instead of self.balance creates a throwaway local under ordinary scoping rules. Worth adding: 'self' is convention, not keyword — but breaking the convention is a review rejection everywhere.
When do you model something as a class versus a dict plus functions?
Class triggers: multiple functions all take the same bundle of state (the same-first-argument smell); the state has INVARIANTS that operations must preserve — a balance that only moves via deposit/withdraw, counters that must update together; you're making many instances of one shape with behavior; or you're building to a lifecycle protocol like sklearn's init/fit/transform. Dict-plus-functions wins when data is transient and shape-flexible — rows flowing through an ETL pipeline rarely deserve classes, and JSON naturally stays dicts at the boundary. The middle ground matters too: a dataclass gives named, typed fields without ceremony when there's state but little behavior. The principle: classes are for state WITH rules; plain data structures are for data in transit. Wrapping everything in classes is as much a smell as wrapping nothing.
Explain sklearn's fit/transform pattern in terms of plain Python classes.
An estimator is a class whose lifecycle maps to three plain mechanisms. __init__ stores CONFIGURATION (hyperparameters) as attributes — no data seen yet. fit(data) computes LEARNED STATE from training data and stores it on self — a scaler's min/max, a model's coefficients — conventionally returning self so calls chain. transform/predict APPLIES that stored state to any data — crucially including data it never learned from, which is what makes train/test separation possible: fit on train only, transform both, and the test set never leaks information into the parameters. Once you see it, the API reads itself: attributes ending in underscore (scaler.min_, model.coef_) are the learned state fit attached; 'NotFittedError' means the None-sentinel state was still unset when transform ran. Being able to hand-write a 15-line MinMaxScaler with exactly this shape is a common and fair interview exercise.
Common Mistakes to Avoid
1) Missing self in a method def — the auto-passed instance has nowhere to land. 2) Bare names where you meant attributes — self.count updates the object; count is a local. 3) Forgetting parentheses: Sensor is the class, Sensor() is a new instance. 4) Initializing shared mutable state on the CLASS body instead of in __init__ — every instance shares one list (next lesson dissects this). 5) Classes for everything — plain data in transit stays dicts/lists. 6) Expecting two constructions with equal args to be the same object — every call builds fresh; aliasing rules from Variables still apply.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: trace self through these three method calls.' • 'Show me a dict+functions design and help me refactor it to a class.' • 'Explain why sklearn's fit returns self.' • 'Give me five designs and make me judge: class or dict?' • 'Interview mode: ask me to build a MinMaxScaler class and grade it.'
Glossary
Class — a blueprint defining a new type's attributes and methods (CapWords names). Object / instance — one value built from a class. __init__ — the initializer run on each construction. self — the instance a method was called on; auto-passed first argument. Attribute — data attached to an instance (self.balance). Method — a function defined in a class, called through an instance. Constructor call — ClassName(args), which allocates and runs __init__. isinstance(x, C) — is x an instance of C? Invariant — a consistency rule operations must preserve. dataclass — stdlib shortcut for state-heavy, behavior-light classes.
Recommended Resources
• Docs: 'Classes' in the official Python tutorial — sections 9.1–9.4 cover today's ground. • Read: the sklearn 'Developing estimators' guide intro — see fit/transform stated as a contract. • Practice: rebuild the closure-based rate limiter from Scope & Closures as a class, and compare the two designs honestly. • Next in DSM: attributes have more machinery than you've seen — Attributes & Methods covers class-level attributes, the shared-mutable trap, and method design.
Recap
✓ class defines a blueprint; calling it builds instances; __init__ furnishes each one. ✓ self IS the instance — acct.deposit(50) passes acct automatically. ✓ Attributes (self.x) hold per-instance state; methods are the operations that keep it consistent. ✓ Same-first-argument-everywhere is the signal to promote dicts+functions to a class. ✓ Objects obey all existing rules: aliasing, scoping, use in lists/dicts/max(key=...). ✓ sklearn/pandas APIs are this lesson: __init__ config, fit learns state, methods apply it. Next up: Attributes & Methods. The blueprint has depth: class attributes vs instance attributes, the shared-mutable trap, and how to design method surfaces that keep state honest.
Run your code to see the output here.