Classes and objects in Python let you bundle data with the functions that work on it. A class is the blueprint; an object is one thing built from that blueprint, holding its own copy of the data while sharing the behaviour.
Concretely, a class statement creates a new type. Calling that type creates an instance. The instance gets its own attributes — its own state — and reaches the class's methods for behaviour. That split is the whole idea, and once it clicks, the rest of Python's object model stops feeling arbitrary.
How do you define your first class?
You write class Name:, then indent the body. Inside, def __init__(self, ...) is the method Python calls when you build an instance, and it is where you set up that instance's attributes. Every other method is one you call on the instance later.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def summary(self):
return f"{self.title} by {self.author} ({self.pages} pages)"
dune = Book("Dune", "Frank Herbert", 412)
print(dune.title)
print(dune.summary())
print("the class of dune is:", type(dune).__name__)
Read the __init__ signature carefully, because it explains a lot. The first parameter is self, and Python passes the new instance into it automatically when you write Book(...). Every other parameter is one you supply. Inside the method, self.title = title does not write to a local variable — it attaches a value to that particular object.
Notice the __ and __ name is a convention with meaning: names beginning and ending with double underscores are called dunder methods, and Python calls them for you at specific moments. You never invoke __init__ yourself in normal code; you let the constructor do it.
There is no keyword like new in Python. Book(...) is a plain function call, which is why a class can be called with the same syntax as any other callable. The name you give the class becomes the name of that function, and the object it produces is an instance of the type it defines.
Do two objects built from one class share data?
No, and this is the point worth proving early. Each instance gets its own attribute storage. Change title on one book and the other book is untouched, even though both were built by the same Book class.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
first = Book("Dune", "Frank Herbert", 412)
second = Book("Piranesi", "Susanna Clarke", 272)
first.title = "Dune (anniversary edition)"
print("first :", first.title)
print("second:", second.title)
print("same object?", first is second)
print("instance storage:", vars(first))
Internally, that per-object storage is a plain dictionary called the instance __dict__. You can look at it directly, and it is a genuinely useful debugging tool: when an object is not behaving as expected, printing vars(obj) or obj.__dict__ tells you exactly what state it believes it is in.
That dictionary is per-instance, and it is created empty for you the moment the object exists. Nothing about it is shared with the class or with any other object, which is why mutating it cannot have side effects elsewhere. If you have ever wondered whether assigning to obj.x in one place could surprise you somewhere else, the answer is no — not as long as the assignment goes through an instance.
How do you check what an object is at runtime?
Three tools cover almost every case. type(obj) gives you the class itself. isinstance(obj, SomeClass) answers yes or no and accepts a tuple of classes for an "any of these" test. vars(obj) gives you the instance dictionary.
class Book:
def __init__(self, title):
self.title = title
class Paperback(Book):
pass
item = Paperback("The Dispossessed")
print("type :", type(item))
print("isinstance of Book:", isinstance(item, Book))
print("type is Book exactly:", type(item) is Book)
print("instance dict:", vars(item))
print("methods you can call:", [n for n in dir(item) if not n.startswith("_")])
isinstance is the one to reach for, because it respects inheritance: a Paperback is a Book, so isinstance(paperback, Book) is True even though type(paperback) is Book is False. When you write code that should accept "a book or a magazine", isinstance says what you mean; type() comparisons say something stricter and usually wrong.
dir(obj) lists everything you can reach on an object — its attributes, its methods, and the dunder methods Python uses behind the scenes. It is a good way to discover what a library object actually supports.
When is a class better than a plain dictionary?
A dictionary holds data and nothing else. Every operation on it is a raw key lookup or a raw assignment, and nothing stops a typo from creating a new key where you meant to read an old one.
A class adds three things: a name for the shape, methods that guarantee the shape stays valid, and a type you can check with isinstance. When the data has rules — a price cannot be negative, a title cannot be empty — you can enforce those rules in one place, inside __init__, and every object built from that class inherits the guarantee.
def make_dict_book(title, pages):
return {"title": title, "pages": pages, "summary": lambda: f"{title} ({pages} pages)"}
class Book:
def __init__(self, title, pages):
if not title.strip():
raise ValueError("title cannot be blank")
if pages <= 0:
raise ValueError("pages must be positive")
self.title = title
self.pages = pages
def summary(self):
return f"{self.title} ({self.pages} pages)"
print(make_dict_book("Dune", 412)["summary"]())
print(Book("Dune", 412).summary())
try:
Book("Dune", 0)
except ValueError as exc:
print("rejected:", exc)
None of this means you should reach for a class on every data blob. A dictionary is the right tool for a bag of unrelated values where nothing needs to be true. Reach for a class when values belong together and when you want the language to help you keep them consistent.
| You need | Reach for |
|---|---|
| a bag of unrelated values, no rules | a plain dictionary |
| related values with rules that must hold | a class |
| the same shape repeated many times | a dataclass (lesson 49) |
| a set of values drawn from a fixed list | a plain string or an enum |
The short version
- A class defines a type. Calling it creates an instance with its own attribute storage.
__init__(self, ...)runs automatically at construction;selfis the new instance, passed in for you.- Assignments inside a method use
self.name = valueto attach state to that one object. - Use
isinstance()for type checks so inheritance still counts, andvars(obj)to see an object's state. - Use a class when data belongs together and has rules; use a dictionary when it does not.
Try it yourself: model a small library
Write a Magazine class with title, issue_number and month. Have __init__ refuse a negative issue number by raising ValueError. Add a label() method that returns something like "Magazine 12, published in March".
Then build a Library class that holds a list of magazines in self.items, with methods to add an item, count the items and list their labels. Test it by creating three magazines, checking that len(library.items) is 3, and confirming that creating a magazine with issue number 0 or -4 raises the error you wrote. Print vars(library) at the end to see everything the object is carrying.