Objects, Dataclasses, and Types
What You'll Learn
How to create your own data types using classes, simplify them with @dataclass, and use type hints throughout.
The Problem Classes Solve
Without classes, related data gets scattered:
# ❌ Related data in separate variables — fragile
name1 = "Alice"
age1 = 30
name2 = "Bob"
age2 = 25
With a class, you bundle data and behavior together:
# ✅ Data grouped in a class
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
print(alice.name) # Alice
Class Basics
class Rectangle:
# __init__ runs when you create a new Rectangle
def __init__(self, width: float, height: float):
self.width = width
self.height = height
# Methods are functions that belong to the class
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# __repr__ — string representation (for debugging)
def __repr__(self) -> str:
return f"Rectangle(width={self.width}, height={self.height})"
# __str__ — human-readable string
def __str__(self) -> str:
return f"{self.width}×{self.height} rectangle"
# Create instances
r1 = Rectangle(10, 5)
r2 = Rectangle(3.5, 7.0)
print(r1.area()) # 50.0
print(r1.perimeter()) # 30.0
print(r1) # 10×5 rectangle
print(repr(r1)) # Rectangle(width=10, height=5)
Inheritance
A subclass inherits everything from the parent and can override or extend it:
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self) -> str: # override parent method
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} says: Meow!"
animals = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy")]
for animal in animals:
print(animal.speak())
Call the parent's method with super():
class Employee(User):
def __init__(self, name, age, email, department):
super().__init__(name, age, email) # call User.__init__
self.department = department
@dataclass — The Modern Way to Write Data Classes
@dataclass auto-generates __init__, __repr__, and __eq__ for you:
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class User:
name: str
age: int
email: str
active: bool = True # default value
tags: list = field(default_factory=list) # mutable default
print(alice == bob) # False (__eq__ generated automatically)
Frozen Dataclasses (Immutable)
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
p.x = 5.0 # FrozenInstanceError — can't modify frozen instances
Use frozen dataclasses for:
- Configuration objects that shouldn't change
- Dictionary keys
- Thread-safe data sharing
Dataclass with Methods
from dataclasses import dataclass
import math
@dataclass
class Circle:
radius: float
def area(self) -> float:
return math.pi * self.radius ** 2
def circumference(self) -> float:
return 2 * math.pi * self.radius
def scale(self, factor: float) -> "Circle":
"""Return a new circle scaled by factor."""
return Circle(self.radius * factor)
c = Circle(5.0)
print(f"Area: {c.area():.2f}") # Area: 78.54
print(f"Scaled: {c.scale(2).radius}") # Scaled: 10.0
Type Hints — Full Picture
# Built-in types
x: int = 5
name: str = "Alice"
height: float = 1.75
active: bool = True
# Collections (Python 3.9+)
items: list[str] = ["a", "b"]
coords: tuple[float, float] = (1.0, 2.0)
tags: set[str] = {"python", "beginner"}
config: dict[str, int] = {"port": 5432}
# Optional — can be None
from typing import Optional
user: Optional[str] = None # str | None in Python 3.10+
result: str | None = None # Python 3.10+ syntax
# Union — multiple possible types (3.10+)
value: int | float | str = 42
# Callable
from typing import Callable
handler: Callable[[int, str], bool] # takes (int, str), returns bool
# Any — disable type checking for this variable
from typing import Any
data: Any = load_from_external_source()
Class vs Dataclass — When to Use Which
| Situation | Use |
|---|---|
| Data container (no complex logic) | @dataclass |
| Need immutable instances | @dataclass(frozen=True) |
| Complex behavior, inheritance hierarchy | Regular class |
| Result object from a function | @dataclass(frozen=True) |
| Config settings | @dataclass(frozen=True) |
Common Mistakes
| Mistake | Fix |
|---|---|
Mutable default in @dataclass | Use field(default_factory=list) |
Forgetting self in methods | All instance methods take self as first arg |
| Creating huge god classes | Split into smaller, focused classes |
Modifying frozen=True instances | They're immutable by design |
Quick Reference
# Regular class
class MyClass:
def __init__(self, x: int):
self.x = x
def method(self) -> str:
return str(self.x)
def __repr__(self) -> str:
return f"MyClass(x={self.x})"
# Dataclass
from dataclasses import dataclass, field
@dataclass
class Record:
name: str
value: float = 0.0
tags: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class Config:
host: str
port: int = 5432
# Type hints
name: str
items: list[str]
config: dict[str, int]
result: int | None