Classes and objects
Bundle data with the functions that work on it.
Why it matters
Most libraries you'll use in AI work hand you objects. The Anthropic SDK gives you a client object with a messages.create method. The reply it returns is an object with .content and .usage. A NumPy array is an object with .shape and .mean(). Once you know how classes work, those dots stop being magic. You can read them, and bundle your own data and logic the same way.
What you'll be able to do
- Define a class with __init__ and create objects from it
- Store data on an object as attributes and act on it with methods
- Explain what self refers to
- Read a dataclass, and judge when writing a class is worth it
What it covers
- Data and the functions that use it
- What self is
- Methods that change the object
- A readable printout with repr
- Reading other people's objects
- Worked example: a small campaign portfolio
- The line that looks done but isn't
Data and the functions that use it
You've stored a campaign as a dictionary and written functions like roas(revenue, spend) that work on that data. The data and the functions belong together, but nothing in the code says so.
A class bundles them. Think of a cookie cutter: one shape, used to make many cookies, each with its own icing. The class is the cutter. It describes what data each thing holds (its attributes) and what it can do (its methods). An object, also called an instance, is one cookie: one particular thing made from the class.
class Campaign: def __init__(self, name, spend, revenue): self.name = name self.spend = spend self.revenue = revenue def roas(self): return self.revenue / self.spend spring = Campaign("Spring Sale", 3120.5, 15840) video = Campaign("Brand Video", 6400, 2870) print(spring.name, round(spring.roas(), 2)) print(video.name, round(video.roas(), 2))
The pieces:
class Campaign:starts the blueprint. Class names use CapWords (EmailCampaign), unlike the snake_case of variables and functions.__init__(two underscores on each side) runs automatically when you create an object. Its job is to set the starting attributes.self.name = namestores the value as an attribute on this particular object.roasis a method: a function defined inside the class. You call it with a dot,spring.roas().Campaign("Spring Sale", 3120.5, 15840)creates a new object and runs__init__with those arguments.
spring and video share the same code but hold different data. Each object keeps its own attributes.
What self is
Every method's first parameter is self: the object the method was called on. When you write spring.roas(), Python runs Campaign.roas(spring) behind the scenes. So inside the method, self is spring, and self.revenue is spring's revenue.
class Campaign: def __init__(self, name, spend, revenue): self.name = name self.spend = spend self.revenue = revenue def roas(self): return self.revenue / self.spend spring = Campaign("Spring Sale", 3120.5, 15840) print(spring.roas()) print(Campaign.roas(spring))
Both lines do the same thing. You always write self as the first parameter in the definition. You never pass it yourself: Python fills it in from whatever is before the dot.
spring.roas(), what is self inside the roas method?Show the answer
The spring object
Python passes whatever is before the dot as self, so self.revenue is spring's own revenue.
Watch the clicks variable. Each call to add runs with self pointing at the same object, so its count builds from 10 to 15 to 18.
Methods that change the object
Methods can read attributes, and they can update them. An attribute doesn't have to come from an argument. __init__ can set a starting value of its own:
class Campaign: def __init__(self, name, budget): self.name = name self.budget = budget self.spend = 0 def record_spend(self, amount): self.spend += amount def remaining(self): return self.budget - self.spend def is_over_budget(self): return self.spend > self.budget search = Campaign("Competitor Terms", 2500) search.record_spend(1200) search.record_spend(1190.75) print(search.remaining()) print(search.is_over_budget())
It prints 109.25, then False. Every campaign starts with a spend of 0 and builds it up through record_spend. The rest of your program doesn't need to know how remaining budget is worked out. It just asks the object.
class Lead: def __init__(self, email): self.email = email self.score = 0 def bump(self): self.score += 10 a = Lead("ana@shop.com") b = Lead("ben@mail.com") a.bump() a.bump() b.bump() print(a.score, b.score)
Show the answer
20 10
Each object has its own score, starting at 0. a was bumped twice, so its score is 20, and b once, so its score is 10. print shows them side by side: 20 10.
A readable printout with __repr__
Print an object and by default you get something like <__main__.Campaign object at 0x10a3f>. Define a __repr__ method that returns a string, and Python uses it whenever it needs to show the object:
class Campaign: def __init__(self, name, spend): self.name = name self.spend = spend def __repr__(self): return f"Campaign(name='{self.name}', spend={self.spend})" print(Campaign("Search", 120)) print([Campaign("Search", 120), Campaign("Email", 40)])
Methods with double underscores on both sides are called "dunder" methods. Python calls them for you: __init__ when an object is created, __repr__ when it's displayed. There are others, but these two are the ones you'll write most.
Dataclasses: less typing for classes that hold data
Many classes mostly hold data. Python's dataclasses module writes __init__ and __repr__ for you from a list of fields. (from ... import loads code from Python's standard library. The modules lesson covers it.)
from dataclasses import dataclass @dataclass class Product: sku: str name: str price: float stock: int = 0 def in_stock(self): return self.stock > 0 mug = Product("MUG-003", "Ceramic Mug", 18) tee = Product("TEE-001", "Organic Cotton Tee", 28, stock=140) print(mug) print(tee.in_stock(), mug.in_stock())
@dataclass on the line above the class is a decorator: it changes the class that follows. sku: str declares a field and notes its expected type. Python doesn't enforce the type. But it documents your intent, and tools and AI assistants use it to spot mistakes.
class Tag: def __init__(self, label): self.label = label def __repr__(self): return f"#{self.label}" print([Tag("sale"), Tag("new")])
Show the answer
[#sale, #new]
Python uses __repr__ to show each object, even inside a list. Each Tag returns # plus its label, so the list shows as [#sale, #new], with no quotes.
Reading other people's objects
You can already read code like this, which you'll write for real in a later lesson:
reply = client.messages.create(model="claude-opus-5-5", max_tokens=500, messages=history) print(reply.content[0].text) print(reply.usage.output_tokens)
Read it one dot at a time:
clientis an object.messagesis an attribute holding another object, andcreateis a method on it.- The reply is an object whose
contentattribute is a list. [0]takes the first item, itself an object with atextattribute.
Every dot is one step: into an attribute, or a method call if parentheses follow.
You've also been using objects all along. "hello".upper() calls the upper method of a str object, and [1, 2].append(3) calls a method of a list object. type("hi") reports <class 'str'> because str is a class.
When to write your own class: when several functions keep passing the same group of data around. Or when you need many things that share the same behavior, such as campaigns, leads or documents in a search index. For a short script, dictionaries and functions are fine. Don't add a class just because you can.
Worked example: a small campaign portfolio
Objects can hold other objects. Build a portfolio that collects campaigns and answers questions about them.
class Campaign: def __init__(self, name, spend, revenue): self.name = name self.spend = spend self.revenue = revenue def roas(self): if self.spend == 0: return 0 return self.revenue / self.spend class Portfolio: def __init__(self): self.campaigns = [] def add(self, campaign): self.campaigns.append(campaign) def total_spend(self): total = 0 for c in self.campaigns: total += c.spend return total def best(self): best = self.campaigns[0] for c in self.campaigns: if c.roas() > best.roas(): best = c return best portfolio = Portfolio() portfolio.add(Campaign("Spring Sale", 3120.5, 15840)) portfolio.add(Campaign("Retargeting", 1890, 5236)) portfolio.add(Campaign("Brand Video", 6400, 2870)) print("Total spend:", portfolio.total_spend()) print("Best ROAS:", portfolio.best().name)
It prints Total spend: 11410.5 and Best ROAS: Spring Sale.
Portfolio keeps a list of Campaign objects in its campaigns attribute. Its methods loop over that list and ask each campaign about itself (c.spend, c.roas()). portfolio.best().name chains two steps: call best(), which returns a Campaign object, then read that object's name. Each class knows about its own data. The code at the bottom reads almost like a description of what you want.
The line that looks done but isn't
Inside __init__, writing name = name does nothing useful. The value has to go on the object: self.name = name. Otherwise it vanishes when __init__ ends, and you get an AttributeError later on.
Practice what you just read
- 7 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Inside a method, what does self refer to?