A class bundles data and behavior together, exactly as in other object-oriented languages, though Python's syntax has a few of its own conventions.
class Car:
def __init__(self, brand, speed=0): # constructor — 'self' is required explicitly
self.brand = brand # instance attribute
self.speed = speed
def accelerate(self, amount): # 'self' must be the first parameter
self.speed += amount
def show_status(self):
print(f"{self.brand} at {self.speed} km/h")
my_car = Car("Toyota")
my_car.accelerate(50)
my_car.show_status() # "Toyota at 50 km/h"
__init__ is Python's constructor — called automatically when Car(...) creates a new instance, used to set up initial attributes. Unlike most languages, Python requires self to be explicitly listed as the first parameter of every instance method — it's not hidden or implicit — and self refers to the specific instance the method was called on, exactly like this in C++/Java, just spelled differently and passed visibly.
Attributes assigned to self (like self.brand) are instance attributes — each object gets its own independent copy. Attributes defined directly in the class body (outside any method) are class attributes, shared by every instance unless individually overridden — a distinction that matters especially for mutable defaults, similar to the mutable default argument trap covered earlier.
Python has no real private/public/protected keywords like C++ or Java — by convention, a single leading underscore (_speed) signals "internal, please don't touch from outside" without technically enforcing it, and this convention-over-enforcement approach is a recurring theme throughout Python's design philosophy.