Class Basics
A class is a blueprint for creating objects. An object is an instance of a class with its own state (attributes) and behavior (methods).
class Dog:
"""A simple Dog class."""
# Class attribute - shared by all instances
species = "Canis lupus familiaris"
def __init__(self, name, age):
"""Initialize a new Dog instance."""
# Instance attributes - unique to each instance
self.name = name
self.age = age
def bark(self):
"""Make the dog bark."""
return f"{self.name} says: Woof!"
def info(self):
return f"{self.name} ({self.age} years old)"
# Create instances
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Access instance attributes
print(dog1.name) # Buddy
print(dog2.age) # 5
# Access class attribute via instance or class
print(dog1.species) # Canis lupus familiaris
print(Dog.species) # Canis lupus familiaris
# Call methods
print(dog1.bark()) # Buddy says: Woof!
print(dog2.info()) # Max (5 years old)
# Modify instance attributes
dog1.age = 4
print(dog1.age) # 4 - only dog1 changed
print(dog2.age) # 5 - dog2 unchanged
# Add new attribute to one instance
dog1.tricks = ["sit", "shake"] # dog2 has no tricks attribute
# isinstance() and type()
print(isinstance(dog1, Dog)) # True
print(type(dog1)) #
print(type(dog1) is Dog) # True
Instance, Class, and Static Methods
class Circle:
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
# Instance method - has access to self
def area(self):
return self.PI * self.radius ** 2
def circumference(self):
return 2 * self.PI * self.radius
# Class method - receives cls, not self
# Common use: alternative constructor
@classmethod
def from_diameter(cls, diameter):
"""Create a Circle from a diameter value."""
return cls(diameter / 2) # cls() is same as Circle()
@classmethod
def unit_circle(cls):
"""Return a circle with radius 1."""
return cls(1)
# Static method - no self or cls
# Just a function that lives in the class namespace
@staticmethod
def is_valid_radius(radius):
return isinstance(radius, (int, float)) and radius > 0
# Instance method - called on an instance
c1 = Circle(5)
print(f"Area: {c1.area():.2f}") # Area: 78.54
print(f"Circumference: {c1.circumference():.2f}")
# Class method - called on the class (or an instance)
c2 = Circle.from_diameter(10)
print(c2.radius) # 5.0
c3 = Circle.unit_circle()
print(c3.radius) # 1
# Static method - called on the class (or an instance)
print(Circle.is_valid_radius(5)) # True
print(Circle.is_valid_radius(-1)) # False
# Summary of differences
# instance_method(self) - can read/write self.attr and cls
# classmethod(cls) - can read/write cls.attr, creates instances
# staticmethod() - no self or cls, pure utility function
@property
@property creates a managed attribute with getter, setter, and deleter logic.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # _name signals "private by convention"
# Getter - accessed like an attribute (no parentheses)
@property
def celsius(self):
return self._celsius
# Setter - runs when you assign: obj.celsius = value
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"Temperature below absolute zero: {value}")
self._celsius = value
# Computed property (read-only - no setter)
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@property
def kelvin(self):
return self._celsius + 273.15
# Deleter
@celsius.deleter
def celsius(self):
del self._celsius
t = Temperature(25)
# Access like attribute (no ())
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
print(t.kelvin) # 298.15
# Assign triggers setter with validation
t.celsius = 100
print(t.fahrenheit) # 212.0
try:
t.celsius = -300 # below absolute zero
except ValueError as e:
print(e) # Temperature below absolute zero: -300
# Practical: lazy computed property with caching
class Circle:
def __init__(self, radius):
self._radius = radius
self._area = None # cache
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
self._area = None # invalidate cache
@property
def area(self):
if self._area is None:
self._area = 3.14159 * self._radius ** 2
return self._area
Dunder Methods
Dunder (double-underscore) methods integrate your class with Python's built-in operations.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
# __str__: human-readable string (used by print() and str())
def __str__(self):
return f'"{self.title}" by {self.author}'
# __repr__: developer/debug string (used by repr() and REPL)
def __repr__(self):
return f'Book(title={self.title!r}, author={self.author!r}, pages={self.pages})'
# __len__: len(obj)
def __len__(self):
return self.pages
# __eq__: == operator
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.title == other.title and self.author == other.author
# __lt__: < operator (enables sorting)
def __lt__(self, other):
return self.title < other.title
# __hash__: required if you define __eq__ and want to use in sets/dicts
# If __eq__ defined, __hash__ is set to None by default (unhashable)
def __hash__(self):
return hash((self.title, self.author))
# __bool__: truth value - bool(obj)
def __bool__(self):
return self.pages > 0
# __contains__: in operator
def __contains__(self, word):
return word.lower() in self.title.lower()
b1 = Book("Python Cookbook", "David Beazley", 706)
b2 = Book("Fluent Python", "Luciano Ramalho", 792)
b3 = Book("Python Cookbook", "David Beazley", 706)
print(b1) # "Python Cookbook" by David Beazley
print(repr(b1)) # Book(title='Python Cookbook', ...)
print(len(b1)) # 706
print(b1 == b3) # True (same title + author)
print(b1 == b2) # False
print(bool(b1)) # True
print("Python" in b1) # True
# Sorting works because __lt__ is defined
books = [b2, b1]
print(sorted(books)) # sorted by title alphabetically
# Can be used in sets/dicts because __hash__ is defined
book_set = {b1, b2, b3}
print(len(book_set)) # 2 (b1 and b3 are equal)
| Dunder | Called by | Description |
|---|---|---|
__init__ | ClassName() | Object initializer |
__str__ | str(obj), print(obj) | Human-readable string |
__repr__ | repr(obj), REPL | Debug string |
__len__ | len(obj) | Length |
__eq__ | obj == other | Equality |
__lt__ | obj < other | Less than (enables sort) |
__hash__ | hash(obj) | Hash value (for dict/set) |
__bool__ | bool(obj), if obj | Truth value |
__contains__ | x in obj | Membership test |
__iter__ | for x in obj | Iterator protocol |
__getitem__ | obj[key] | Subscript access |
__enter__/__exit__ | with obj | Context manager |
Operator Overloading
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# Arithmetic operators
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): # v1 - v2
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): # v * 3
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # 3 * v (reflected mul)
return self.__mul__(scalar)
def __neg__(self): # -v
return Vector(-self.x, -self.y)
def __abs__(self): # abs(v) - magnitude
return (self.x**2 + self.y**2) ** 0.5
def __eq__(self, other):
return self.x == other.x and self.y == other.y
v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2) # Vector(3, 7)
print(v1 - v2) # Vector(1, -1)
print(v1 * 3) # Vector(6, 9)
print(3 * v1) # Vector(6, 9) - uses __rmul__
print(-v1) # Vector(-2, -3)
print(abs(v1)) # 3.605...
# Sequence protocol
class NumberRange:
def __init__(self, start, end):
self.start = start
self.end = end
def __contains__(self, n):
return self.start <= n <= self.end
def __len__(self):
return self.end - self.start + 1
def __iter__(self):
return iter(range(self.start, self.end + 1))
r = NumberRange(1, 10)
print(5 in r) # True
print(11 in r) # False
print(len(r)) # 10
print(list(r)) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Classes vs @dataclass
For classes that mainly store data, @dataclass (Python 3.7+) eliminates boilerplate by auto-generating __init__, __repr__, and __eq__.
from dataclasses import dataclass
# Manual class - lots of boilerplate
class PointManual:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
return isinstance(other, PointManual) and self.x == other.x and self.y == other.y
# @dataclass - same thing, less code
@dataclass
class Point:
x: float
y: float
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1) # Point(x=1.0, y=2.0) - __repr__ auto-generated
print(p1 == p2) # True - __eq__ auto-generated
# Use regular classes for:
# - Classes with significant behavior (methods)
# - Classes needing custom __init__ logic
# - Inheritance hierarchies
# Use @dataclass for:
# - Simple data containers (DTO, value objects)
# - When you want auto-generated __init__, __repr__, __eq__
# See the Dataclasses tutorial for full details