Skip to main content

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
email1 = "alice@example.com"

name2 = "Bob"
age2 = 25
email2 = "bob@example.com"

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

alice = User("Alice", 30, "alice@example.com")
bob = User("Bob", 25, "bob@example.com")

print(alice.name) # Alice
print(bob.email) # bob@example.com

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

alice = User("Alice", 30, "alice@example.com")
bob = User(name="Bob", age=25, email="bob@example.com", active=False)

print(alice) # User(name='Alice', age=30, email='alice@example.com', active=True, tags=[])
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

SituationUse
Data container (no complex logic)@dataclass
Need immutable instances@dataclass(frozen=True)
Complex behavior, inheritance hierarchyRegular class
Result object from a function@dataclass(frozen=True)
Config settings@dataclass(frozen=True)

Common Mistakes

MistakeFix
Mutable default in @dataclassUse field(default_factory=list)
Forgetting self in methodsAll instance methods take self as first arg
Creating huge god classesSplit into smaller, focused classes
Modifying frozen=True instancesThey'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

What's Next

Lesson 4: Type Hints and Docstrings