OOP: Classes & Objects

Object-Oriented Programming (OOP) is a way of structuring code around classes — blueprints that bundle related data and behavior together — and objects, which are individual instances built from those blueprints. Python is fully object-oriented, and understanding classes is essential for writing larger, well-organized programs.

Defining a Class

A class is defined with the class keyword followed by a name, conventionally written in CapitalizedWords (PascalCase):

class Car:
    pass   # an empty class body, for now

The __init__ Constructor and self

The __init__ method is a special method that Python calls automatically whenever a new object is created from the class. It is used to set up the object's initial state. Its first parameter is always self, which refers to the specific object being created or acted upon.

Every instance method in Python takes self as its first parameter because Python does not have any hidden or implicit way to know which object a method call belongs to — it must be passed in explicitly. When you write my_car.describe(), Python actually calls Car.describe(my_car) behind the scenes, and self is exactly how the method receives that instance.

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model

Instance Attributes vs. Class Attributes

An instance attribute belongs to one specific object and is usually set inside __init__ using self.attribute = value. A class attribute belongs to the class itself and is shared by every instance, unless a particular instance overrides it.

class Car:
    wheels = 4   # class attribute: shared by every Car instance

    def __init__(self, make, model):
        self.make = make     # instance attribute: unique per object
        self.model = model   # instance attribute: unique per object

car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")

print(car1.make, car1.wheels)   # Toyota 4
print(car2.make, car2.wheels)   # Honda 4

# changing the class attribute affects every instance that hasn't overridden it
Car.wheels = 6
print(car1.wheels)             # 6

Defining and Calling Instance Methods

An instance method is just a function defined inside a class that operates on self. It is called using dot notation on an object.

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model

    def describe(self):
        return f"{self.make} {self.model}"

my_car = Car("Toyota", "Corolla")
print(my_car.describe())   # Toyota Corolla

Creating an Object (Instance) from a Class

Calling a class like a function — Car(...) — creates a new object, passing arguments straight through to __init__. This process is called instantiation, and the resulting object is an instance of that class.

my_car = Car("Tesla", "Model 3")
print(type(my_car))   # <class '__main__.Car'>

Controlling How an Object Prints with __str__

By default, printing an object shows an unhelpful memory address. Defining a __str__ method lets you control exactly what print(my_object) displays instead.

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model

    def __str__(self):
        return f"Car({self.make} {self.model})"

my_car = Car("Toyota", "Corolla")
print(my_car)   # Car(Toyota Corolla) -- without __str__ this would print something like <__main__.Car object at 0x...>

Full Worked Example: A Car Class

class Car:
    wheels = 4   # class attribute shared by all cars

    def __init__(self, make, model):
        self.make = make    # instance attribute
        self.model = model  # instance attribute

    def describe(self):
        return f"This car is a {self.make} {self.model} with {self.wheels} wheels."

    def __str__(self):
        return f"Car({self.make} {self.model})"


my_car = Car("Toyota", "Corolla")
print(my_car.describe())   # This car is a Toyota Corolla with 4 wheels.
print(my_car)              # Car(Toyota Corolla)
Next steps: Head to OOP: Inheritance to learn how one class can build on another.