Classes and Objects in Python
A practical introduction to Python classes — defining a class, __init__ and self, instance vs class attributes, methods, __repr__, and a first look at inheritance.
What you'll learn
- ✓How to define a class with the class keyword
- ✓What __init__ and self actually do
- ✓The difference between instance and class attributes
- ✓How methods work and what self really is
- ✓Why __repr__ is worth writing for every class
- ✓A first look at inheritance
Prerequisites
- •Comfortable with Functions and Dictionaries
A class is a blueprint for objects that share the same structure and behaviour. Once you’ve described a User once, you can create a thousand users that all know how to validate themselves, format themselves, and answer questions about themselves. Classes are not the only way to organise data in Python — dictionaries and functions take you a long way — but past a certain point, classes are the cleanest representation of “things that have state and verbs.”
What is a class?
A class bundles data (attributes) with behaviour (methods) and gives the bundle a name. An object is one specific instance of that class — one concrete User, one concrete Order.
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
print(f"{self.name} says woof!")
rex = Dog("Rex", "Labrador")
rex.bark() # Rex says woof!
Dog is the class. rex is an object — an instance of Dog. The class is the recipe; the object is the cake.
__init__ and self
__init__ is the initialiser — it runs every time you create a new instance. Its job is to set up the object’s initial state.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.x, p.y) # 3 4
Two things to notice:
- You did not call
__init__directly. WritingPoint(3, 4)creates the object and calls__init__for you. - The first parameter is
self. It refers to the object being constructed.self.x = xstores the argumentxon the object as an attribute namedx.
self is the convention for the first parameter of every method. It is not a keyword — you could legally name it me or this — but every Python programmer expects self, and editors and tools assume it. Use self.
Calling the class does the work for you:
- Python creates a new empty object of type
Point. - It calls
Point.__init__(new_object, 3, 4)— passing the new object asself. __init__populates the object and returns nothing.- The new object is what
Point(3, 4)evaluates to.
That’s the entire mechanism.
Methods
A method is a function defined inside a class. Its first parameter is always self, and inside the body you read and write attributes through it.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def scale(self, factor):
self.width *= factor
self.height *= factor
r = Rectangle(3, 4)
print(r.area()) # 12
r.scale(2)
print(r.area()) # 48
When you call r.area(), Python translates it to Rectangle.area(r). The r becomes self inside the method. This is the entire trick — methods are functions whose first argument is filled in by the dot.
Methods that don’t return a value (like scale above) return None, like any function. Methods that produce a useful value should return it.
Instance vs class attributes
The attributes you set on self belong to that specific instance. Each Rectangle has its own width and height. Sometimes, though, you want an attribute that belongs to the class itself — shared by every instance.
class Dog:
species = "Canis familiaris" # class attribute
def __init__(self, name):
self.name = name # instance attribute
a = Dog("Rex")
b = Dog("Fido")
print(a.species, b.species) # Canis familiaris Canis familiaris
print(a.name, b.name) # Rex Fido
Class attributes are useful for constants and defaults shared across all instances. They live on the class object itself, not on each instance.
A trap to know about: if a class attribute is a mutable object — a list, dict, or set — it will be shared by every instance, and a mutation through one instance is visible to all the others.
class Inbox:
messages = [] # SHARED across all instances — almost always a bug
def add(self, msg):
self.messages.append(msg)
a = Inbox()
b = Inbox()
a.add("hi")
print(b.messages) # ['hi'] — oops
The fix is to set mutable state in __init__, so each instance gets its own:
class Inbox:
def __init__(self):
self.messages = []
This is the same shape as the famous “mutable default argument” trap — see Default and Keyword Arguments.
__repr__ — give your objects a useful printout
By default, print(obj) gives you something like <__main__.Point object at 0x10a4f8d50> — useless. __repr__ lets you control what an object looks like when printed or shown at the REPL.
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})"
p = Point(3, 4)
print(p) # Point(x=3, y=4)
print([p, Point(0, 0)]) # [Point(x=3, y=4), Point(x=0, y=0)]
The convention for __repr__ is to return a string that looks like a Python expression which would recreate the object. Point(x=3, y=4) passes the eye test. It’s the small habit that makes every debugging session easier.
__repr__ is one of Python’s dunder methods — short for “double underscore.” There are many more (__eq__, __len__, __iter__, __add__) and they hook into language operators. __repr__ is the one to start with.
Try it yourself. Write a BankAccount class with owner and balance attributes, methods deposit(amount) and withdraw(amount) (refuse negative amounts and overdrafts), and a __repr__ that prints BankAccount(owner='Alice', balance=100). Create an account, deposit and withdraw, and print it.
Methods that compute, methods that mutate
A useful habit is to separate queries (methods that compute and return without changing state) from commands (methods that change state, typically returning None).
class Counter:
def __init__(self):
self.value = 0
def increment(self): # command
self.value += 1
def is_even(self): # query
return self.value % 2 == 0
A method that does both — changes state and returns something interesting — is harder to reason about. Not forbidden, but worth pausing over when you find yourself writing one.
A worked example
A small ShoppingCart class that models a real interaction:
class ShoppingCart:
tax_rate = 0.08 # class attribute — shared
def __init__(self, owner):
self.owner = owner
self.items = [] # list of (name, price)
def add(self, name, price):
if price < 0:
raise ValueError("price cannot be negative")
self.items.append((name, price))
def remove(self, name):
self.items = [item for item in self.items if item[0] != name]
def subtotal(self):
return sum(price for _, price in self.items)
def total(self):
return round(self.subtotal() * (1 + self.tax_rate), 2)
def __repr__(self):
return f"ShoppingCart(owner={self.owner!r}, items={len(self.items)})"
cart = ShoppingCart("Alice")
cart.add("book", 12.99)
cart.add("pen", 1.50)
print(cart) # ShoppingCart(owner='Alice', items=2)
print(cart.subtotal()) # 14.49
print(cart.total()) # 15.65
cart.remove("pen")
print(cart.total()) # 14.03
Notice the shape: __init__ sets up state, command methods mutate it, query methods compute from it, and __repr__ makes the object printable. Most well-behaved classes look like this.
Inheritance, briefly
A class can inherit from another, taking on all its methods and attributes and optionally adding or overriding some.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"
for pet in [Dog("Rex"), Cat("Whiskers")]:
print(pet.speak())
# Rex says woof!
# Whiskers says meow!
Dog(Animal) means Dog inherits from Animal. Dog gets Animal’s __init__ for free — and overrides speak with its own version.
When a subclass needs to extend the parent’s __init__ rather than replace it, use super():
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, reports):
super().__init__(name, salary)
self.reports = reports
super().__init__(...) calls the parent’s initialiser, then the subclass adds its own bits. This is the standard pattern.
Inheritance is powerful and easy to overuse. The rule of thumb: reach for it when there is a genuine “is-a” relationship — a Manager is an Employee. When you just want to share some helper code, prefer composition or plain functions.
Try it yourself. Create a Vehicle class with make, model, and a describe() method. Then create Car(Vehicle) that adds a num_doors attribute, calls super().__init__ to set the inherited attributes, and overrides describe() to include the door count.
When to reach for a class
Use a class when:
- You have data and behaviour that naturally belong together
- You’re going to create many instances of the same shape
- You need to track state across a sequence of method calls
- A dictionary would technically work but the access patterns are starting to look repetitive
Don’t reach for a class when a single function or a small dictionary does the job — and don’t be afraid to start with a dictionary and migrate to a class once the operations on it start to repeat. Python lets you change your mind cheaply.
For data-only objects (no behaviour, just named fields) you’ll eventually meet @dataclass, which writes __init__, __repr__, and __eq__ for you. We’ll cover it in a later post.
Recap
You now know:
- A class is defined with
class Name:and creates a blueprint for objects __init__sets up new instances;selfrefers to the instance being acted on- Instance attributes live on the object; class attributes are shared
- Methods are functions whose first parameter is
self, filled in by the dot __repr__makes objects printable and is worth writing for every classclass Child(Parent):inherits;super().__init__(...)chains to the parent
Next steps
The next post takes a small but valuable detour into lambda functions — Python’s syntax for anonymous one-line functions. They’re a natural companion to sorted, map, and filter, and seeing where they help (and where they don’t) sharpens your sense of when a regular def is the right call.
→ Next: Lambda Functions in Python
Questions or feedback? Email codeloomdevv@gmail.com.