Object-oriented programming lets you model real things as classes that bundle data and behaviour together. Python's OOP model is flexible: classes are objects, attributes are dynamic, and most of the language's special behaviour comes from dunder (double-underscore) methods.
Your first class
A class is defined with the class keyword. The body usually contains an __init__ method that runs when you create a new instance, plus other methods that describe behaviour. The first argument of every method is conventionally named self, the instance itself.
class Dog:
def __init__(self, name: str, breed: str):
self.name = name
self.breed = breed
self.tricks = []
def teach(self, trick: str) -> None:
self.tricks.append(trick)
def show_tricks(self) -> None:
print(f"{self.name} knows: {', '.join(self.tricks) or 'nothing yet'}")
rex = Dog("Rex", "Labrador")
rex.teach("sit")
rex.teach("roll over")
rex.show_tricks()
Instance attributes versus class attributes
Attributes defined inside __init__ are instance attributes. Each instance gets its own copy. Attributes defined directly in the class body are class attributes, shared by every instance. Use class attributes for constants and defaults.
class Circle:
pi = 3.14159265358979 # class attribute, shared
def __init__(self, radius: float):
self.radius = radius # instance attribute
def area(self) -> float:
return Circle.pi * self.radius ** 2
c = Circle(5)
print(c.area()) # 78.5398...
print(c.pi) # 3.14159... (read from class)
Circle.pi = 3.14 # change for all instances
print(c.area()) # 78.5
Dunder methods make objects Pythonic
Dunder (double-underscore) methods define how built-in operators and functions interact with your objects. Implement __repr__ for a developer-readable string, __eq__ for equality, __lt__ for ordering, and __len__ for the len() function.
class Vector:
def __init__(self, x: float, y: float):
self.x, self.y = x, y
def __repr__(self) -> str:
return f"Vector({self.x!r}, {self.y!r})"
def __eq__(self, other) -> bool:
return isinstance(other, Vector) and (self.x, self.y) == (other.x, other.y)
def __add__(self, other) -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __len__(self) -> int:
return int((self.x ** 2 + self.y ** 2) ** 0.5)
v = Vector(3, 4)
print(v) # Vector(3, 4)
print(len(v)) # 5
print(v + Vector(1, 2)) # Vector(4, 6)
Properties for controlled access
Use the @property decorator to expose a method as a read-only attribute. Combine @property with @x.setter for a writable, validated attribute. Properties let you start with a simple public attribute and add validation later without changing the call site.
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius
@property
def fahrenheit(self) -> float:
return self.celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value: float) -> None:
self.celsius = (value - 32) * 5 / 9
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t.celsius) # 0.0
Dataclasses cut boilerplate
The @dataclass decorator generates __init__, __repr__, and __eq__ for you. It is the cleanest way to write a class whose main job is to hold data. Frozen dataclasses are immutable, like a named tuple with methods.
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
tags: list[str] = field(default_factory=list)
p = Point(3, 4)
p.tags.append("origin")
print(p) # Point(x=3, y=4, tags=['origin'])
Common pitfalls with classes
Three traps appear repeatedly when newcomers adopt OOP. First, mutable class attributes are shared by every instance, so class Cat: tricks = []; makes every cat share the same list and adding to one adds to all. Move the list into __init__ as self.tricks = [] to give each instance its own. Second, forgetting self in a method signature silently turns the method into an unbound function, so calling obj.foo() raises TypeError: takes 0 positional arguments. Always write def foo(self, ...):. Third, customising __eq__ without __hash__ silently breaks the object as a dict key or set element. If your objects compare by value, also define __hash__ = None for mutability or implement __hash__ consistently. Even better, freeze the class with @dataclass(frozen=True) and let the decorator handle the rest.
Comparison with JavaScript classes and C++ classes
If you come from JavaScript, Python classes look familiar but the semantics differ. JavaScript classes are syntactic sugar over prototypes; Python classes are objects themselves, instances of the metaclass type. The most visible consequence is that you can attach methods to a class at runtime, replace a method on a single instance, or even replace the class entirely with a new one. C++ classes are more constrained: methods must be declared inside the class, fields have explicit access modifiers (public, protected, private), and inheritance is locked at compile time. Python's underscore convention (_name, __name) is enforced for __name via name mangling, but otherwise it is a social contract. C++ has multiple inheritance with full virtual dispatch; Python does too, but uses the C3 linearisation algorithm to pick the method resolution order.
Best practices for class design in 2026
Three habits keep class hierarchies maintainable. First, prefer composition over inheritance. A Car that contains a Engine object is easier to test than a Car that inherits from Engine. Second, write small classes that do one thing. If your class has more than a handful of responsibilities, split it into smaller classes that collaborate. Third, dataclasses are the default for data containers. Replace plain classes that only hold fields with @dataclass, and use frozen=True for value objects. For richer behaviour, attrs and pydantic extend the idea with validators and serializers. With these habits, your classes become predictable building blocks instead of tangled webs of state.
Summary
Classes bundle data and behaviour. Instance attributes live on self. Class attributes are shared. Dunder methods integrate your objects with the language. Properties and dataclasses cut boilerplate. The next lesson covers inheritance, the Method Resolution Order, and abstract base classes.