Classes and objects: bundling data with behavior
You have actually been using objects this whole course without naming them. A string is an object; that's why "hello".upper() works — upper is a method (a function attached to the object). A list is an object; nums.append(3) calls the list's append method. This lesson reveals what was behind that dot all along, and teaches you to make your own objects. You don't need to become an object-oriented wizard — but the next guides hand you objects constantly (an API client, a request, a response, a model), and to use them you must be able to read a class and create instances of it. That's the goal.
Why objects exist
Picture modeling a dog in a program. A dog has data — a name, an age — and it can do things — bark, have a birthday. With what you know so far, you'd scatter that across loose variables and functions:
dog_name = "Rex"
dog_age = 3
def bark(name):
return name + " says Woof!"
def have_birthday(age):
return age + 1
This works for one dog. For fifty dogs it falls apart: fifty name/age variable pairs, and functions that don't know which dog they belong to. The data and the behavior that belong together are floating apart. An object bundles them. A Dog object carries its own name and age and knows how to bark — data and behavior packaged into one self-contained thing. That bundling is the whole point of objects.
:::note The two words to keep straight
A class is the blueprint — it describes what every dog has and can do, written once. An object (also called an instance) is a real dog built from that blueprint. One Dog class; as many dog objects as you like, each with its own name and age. Class = cookie cutter; object = each cookie.
:::
Defining a class
Here is a Dog class. Read it once, top to bottom — then we'll dissect every piece:
class Dog:
def __init__(self, name, age):
self.name = name # store this dog's name on the object
self.age = age # store this dog's age on the object
def bark(self):
return self.name + " says Woof!"
def have_birthday(self):
self.age = self.age + 1
And here is how you use it — create an instance and interact with it:
rex = Dog("Rex", 3) # build a Dog object; __init__ runs with name="Rex", age=3
print(rex.name) # Rex — read an attribute
print(rex.bark()) # Rex says Woof! — call a method
rex.have_birthday() # changes rex's own age
print(rex.age) # 4
Now the pieces, one at a time.
class
class Dog:
The class keyword starts a blueprint, followed by the class name (capitalized by convention — Dog, BankAccount, HttpClient). Everything indented under it belongs to the class.
init: the setup method
def __init__(self, name, age):
self.name = name
self.age = age
__init__ (say "dunder init," short for "double underscore init") is a special method Python calls automatically the moment you create an object. Its job is to initialize — to set up the new object's starting data. When you write Dog("Rex", 3), Python builds a blank dog and immediately runs __init__ with name="Rex" and age=3. You never call __init__ directly; creating the object calls it for you.
self: the object itself
This is the one piece that trips up every beginner, so go slowly. self is the object the method is working on. When you call rex.bark(), Python automatically passes rex in as self. So inside bark, self is rex — and self.name means "this particular dog's name."
selfis the first parameter of every method, always. You write it in thedef, but you never pass it yourself — Python fills it in from whatever object is before the dot.rex.bark()becomesbark(rex)behind the scenes, withrexlanding inself.self.name = namemeans "store the incomingnameon this object, under its ownnameattribute." That's how each dog remembers its own data separately.
rex = Dog("Rex", 3)
fido = Dog("Fido", 7)
print(rex.bark()) # Rex says Woof! — self is rex, self.name is "Rex"
print(fido.bark()) # Fido says Woof! — self is fido, self.name is "Fido"
Same bark method, two different objects, two different results — because self points at whichever dog you called it on. That is the magic the dot was hiding.
:::tip One sentence for self
self means "the object this method was called on." rex.bark() runs bark with self = rex, so every self.something reads or writes rex's own data. You declare self but never pass it — the dot before the method name supplies it.
:::
Attributes and methods
Two more terms, now that you've seen them in action:
- An attribute is a piece of data stored on an object —
rex.name,rex.age. You read or set it with the dot:rex.agegets it,rex.age = 4sets it. - A method is a function defined inside the class —
bark,have_birthday. You call it with the dot and parentheses:rex.bark(). A method usually works with the object's attributes throughself.
have_birthday shows a method changing its object's data: self.age = self.age + 1 reads this dog's current age and stores the incremented value back on the same dog. Call rex.have_birthday() and only rex's age goes up — fido is untouched.
A second, more "real" example
Classes shine for things with state that changes over time — a bank account, a shopping cart, a game character. Here's an account, which is much closer to the objects you'll meet in the next guides:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance # default starts at 0
def deposit(self, amount):
self.balance = self.balance + amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds") # refuse, don't go negative
self.balance = self.balance - amount
def summary(self):
return self.owner + " has $" + str(self.balance)
acct = BankAccount("Ada") # balance defaults to 0
acct.deposit(100)
acct.withdraw(30)
print(acct.summary()) # Ada has $70
print(acct.balance) # 70
Notice the familiar tools all reappear inside a class: a default parameter (balance=0), a method calling raise to refuse bad input, string conversion with str(). A class doesn't replace what you know — it organizes it around a thing.
You'll mostly READ classes, not write them
Be honest about the goal. In the guides ahead you will rarely design elaborate class hierarchies. What you'll do constantly is use objects other people defined:
# Modern AI guide (illustrative): an SDK gives you a client OBJECT
client = Anthropic(api_key="...") # create an instance
response = client.messages.create(...) # call a method on it
print(response.content) # read an attribute off the result
You can already read every line of that: create an instance, call a method with the dot, read an attribute. That's the entire payoff of this lesson — the object-oriented surface of every modern library is now legible to you.
Why it matters
Objects are how nearly all real software is organized, because bundling data with the behavior that acts on it scales in a way loose variables never do. You don't have to love OOP or design deep hierarchies — but class, __init__, self, attributes, and methods are the vocabulary of every SDK, framework, and library you're about to touch. When the AI guide hands you a client and the web guide hands you a request, you'll know exactly what they are: objects, made from a class, carrying their own data and methods. No magic left.
Where this leads: SDK clients, request/response objects, and framework models are all just instances of classes — which you can now read fluently. See Where this leads next.
Practice
Define a small class and exercise it. The test calls a method that returns a value:
Define a class Counter with __init__ that sets self.count = 0, a method increment() that adds 1 to self.count, and a method value() that returns self.count. Then complete run_counter(n): create a Counter, call increment() n times, and return its value(). run_counter(3) should return 3; run_counter(0) returns 0.