Why classes exist
So far you have used values like numbers, strings, and lists. Each of those is a kind of thing with its own behavior: a string knows how to turn itself uppercase, a list knows how to append. A class lets you define your own kind of thing, with its own data and its own behavior, bundled together under one name.
The reason to do this is bundling. Real programs deal with concepts that are made of several pieces of data that always travel together. A point on a screen is an x and a y. A bank account is a balance plus an owner name. When those pieces belong together, and when there are operations that naturally act on them, a class keeps the data and the operations in one place instead of scattering them across loose variables and functions.
Class versus object
A class is a blueprint. It describes what data a thing holds and what it can do, but it is not itself a concrete thing. Think of the class as the definition of “what a point is.”
An object (also called an instance) is one concrete thing built from that blueprint. From one Point class you can build many point objects, each with its own x and y. The class is written once; objects are made from it as many times as you need.
class Point: # the blueprint
pass
p = Point() # p is an object: one instance of Point
q = Point() # q is a different object
print(p is q) # -> False (two separate things)
pass is a placeholder that means “empty body.” This class does nothing yet. Next we give it data.
__init__ and self
When you create an object, Python can run setup code to fill in its starting data. That setup code lives in a special method named __init__ (two underscores on each side; say it “dunder init,” short for “double underscore”). A method is just a function that belongs to a class.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.x) # -> 3
print(p.y) # -> 4
Two things need explaining here.
self is the object being worked on. When you write Point(3, 4), Python creates a fresh empty object and passes it into __init__ as the first argument, self. Inside the method, self.x = x means “store the value x onto this particular object under the name x.” You never pass self yourself; Python supplies it. It is the first parameter of every method by convention.
An attribute is a piece of data stored on an object. p.x reads the attribute x from the object p. Because each object has its own self, each object has its own attributes:
a = Point(0, 0)
b = Point(10, 20)
print(a.x, b.x) # -> 0 10 (independent)
Adding methods
A method is a function defined inside the class. It also takes self first, so it can read and change the object’s own attributes.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance_to_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p.distance_to_origin()) # -> 5.0
p.distance_to_origin() calls the method with p automatically bound to self. The method reads self.x and self.y, which are 3 and 4, and returns 5.0. Attributes are the object’s nouns; methods are its verbs.
__repr__: readable printing
By default, printing an object gives something unhelpful:
p = Point(3, 4)
print(p) # -> <__main__.Point object at 0x104f2e990>
That hex number is just the object’s memory address; it tells you nothing useful. Define __repr__ (dunder repr, short for “representation”) to control how the object shows up. It must return a string.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
print(Point(3, 4)) # -> Point(x=3, y=4)
A good __repr__ returns a string that shows what the object is, ideally one that looks like the code you would write to recreate it. This pays off constantly when debugging, because printing a list of objects now shows real information.
Worked example: a Stack
A stack is a collection where you add and remove items from the same end, so the last item in is the first item out (called LIFO, “last in, first out”). Think of a stack of plates: you put a plate on top and take a plate off the top. Stacks show up everywhere, so building one is a good way to see a class do real work.
Picture three items stacked bottom to top, with the top at the right:
graph LR
A["1 (bottom)"] --> B["2"] --> C["3 (top)"]
push adds a new node on the right; pop removes from the right. The bottom never moves.
We will store the items in a Python list and expose four operations: push (add), pop (remove and return the top), peek (look at the top without removing), and is_empty.
class Stack:
def __init__(self):
self._items = [] # start empty
def push(self, value):
self._items.append(value) # add to the top (end of list)
def pop(self):
return self._items.pop() # remove and return the top
def peek(self):
return self._items[-1] # last item, without removing
def is_empty(self):
return len(self._items) == 0
def __len__(self):
return len(self._items)
def __repr__(self):
return f"Stack({self._items})"
Using it:
s = Stack()
s.push(1)
s.push(2)
s.push(3)
print(s) # -> Stack([1, 2, 3])
print(s.peek()) # -> 3
print(s.pop()) # -> 3
print(s.pop()) # -> 2
print(len(s)) # -> 1
print(s.is_empty()) # -> False
The list’s end is the “top” of the stack. Appending to the end and popping from the end are both fast.
Cost
Because a Python list appends and pops at its end in amortized O(1) time (constant time, not depending on how many items are stored), every stack operation here is O(1) time. peek, is_empty, and __len__ are also O(1). Space is O(n) for n stored items, since we hold all of them. “Amortized” means an occasional single append is slower when the list grows its internal storage, but averaged over many appends the cost per operation is still constant.
Dunder methods: __len__ and __eq__
Names wrapped in double underscores are dunder methods (also called special or magic methods). Python calls them for you when you use built-in syntax on your object. You already saw __init__ (called on construction) and __repr__ (called when printing). Two more that come up early:
__len__ lets len(obj) work. We added it to Stack above, so len(s) returns the number of items.
__eq__ defines what == means for your objects. Without it, two objects are equal only if they are literally the same object in memory, which is rarely what you want.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
print(Point(1, 2) == Point(1, 2)) # -> True (same coordinates)
print(Point(1, 2) == Point(9, 9)) # -> False
Without __eq__, that first comparison would be False, because the two points are separate objects even though their contents match. Defining __eq__ says equality means “same coordinates.”
Encapsulation
Encapsulation is the idea of keeping an object’s internal data private to the object and interacting with it only through the methods the class provides. In the Stack, the list is named self._items. A single leading underscore is a convention that means “this is internal; do not touch it from outside.” Python does not forbid access, but the underscore signals intent.
The payoff is that users of the stack call push and pop and never depend on the fact that a list is used inside. If you later swapped the list for a different storage mechanism, code that only used the public methods would keep working. Encapsulation lets the inside change without breaking the outside.
When to use a class versus a dict
A dictionary (dict) also groups named data together, so it is fair to ask when you need a class at all.
# a point as a dict
p = {"x": 3, "y": 4}
print(p["x"]) # -> 3
Reach for a dict when you have plain data with no attached behavior, when the set of keys is not fixed or is decided at runtime, or when you are passing a bag of values around briefly. Dicts are quick and flexible.
Reach for a class when the data has behavior that belongs with it (methods), when there is a fixed shape you want to state clearly and reuse, when you want meaningful printing (__repr__), custom equality (__eq__), or the safety of encapsulation, or when you will create many instances of the same kind of thing. If you find yourself writing functions that all take the same dict as their first argument and operate on it, that is a sign the data and those functions want to be a class.
Common pitfalls
-
Forgetting
self. Every method’s first parameter must beself, and inside the method you must useself.x, not barex, to reach an attribute. Writingxrefers to a local variable, not the object’s data. -
Calling a method without parentheses.
p.distance_to_originis the method object itself;p.distance_to_origin()actually calls it. Forgetting the parentheses gives you the function, not its result. -
Confusing class-level and instance-level data. Assign attributes inside
__init__withself.x = ...so each object gets its own copy. A mutable value assigned directly in the class body (likeitems = []outside any method) is shared by every instance, which almost always causes surprising bugs. Initialize per-object data in__init__. -
Assuming
==works for free. Until you define__eq__,==compares object identity, not contents. -
popon an empty stack. Callingpoporpeekwhen there are no items raises an error, because the underlying list is empty. In real code, checkis_empty()first or handle the error.
Practice
-
Write a
Pointclass with__init__,__repr__, and a methodtranslate(dx, dy)that returns a newPointmoved bydxanddy. Verify that the original point is unchanged. -
Extend the
Stackclass with a__eq__method so that two stacks are equal when they hold the same items in the same order. Test it on two stacks you build with the same pushes. -
Build a
Queueclass (first in, first out: items leave in the order they arrived) withenqueueanddequeuemethods and a__len__. Decide which end of the list each operation should use, and note the Big-O cost of your choice.