Modern Python Types
Dataclass Introduction
Writing boilerplate code for classes that mainly store data is tedious and error-prone. Dataclasses automatically generate init, repr, and eq methods from type-annotated fields, reducing code while maintaining full type safety and IDE support.
Why Use Dataclasses?
- Less boilerplate code
- Automatic
__init__,__repr__,__eq__generation - Type hints integration
- Mutable by default, but can be frozen
- Works with inheritance
Basic Dataclass
# Basic dataclass definition
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str
# Create instances - __init__ is auto-generated
person1 = Person("Alice", 30, "alice@example.com")
person2 = Person("Bob", 25, "bob@example.com")
# __repr__ is auto-generated
print(person1)
# __eq__ is auto-generated
print(person1 == person2) # False
print(person1 == Person(person1.name, person1.age, person1.email)) # True
# Basic dataclass definition
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str
# Create instances - __init__ is auto-generated
person1 = Person("Dana", 42, "dana@example.com")
person2 = Person("Bob", 25, "bob@example.com")
# __repr__ is auto-generated
print(person1)
# __eq__ is auto-generated
print(person1 == person2) # False
print(person1 == Person(person1.name, person1.age, person1.email)) # True
# Basic dataclass definition
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str
# Create instances - __init__ is auto-generated
person1 = Person("Sam", 19, "sam@example.com")
person2 = Person("Bob", 25, "bob@example.com")
# __repr__ is auto-generated
print(person1)
# __eq__ is auto-generated
print(person1 == person2) # False
print(person1 == Person(person1.name, person1.age, person1.email)) # True
person1 ← Person(name='Alice', age=30, email='alice@example.com')
6class Person:7 name(empty): str8 age(empty): int9 email(empty): str1011# Create instances - __init__ is auto-generated12person1→ Person(name='Alice', age=30, email='alice@example.com') = Person("Alice", 30, "alice@example.com")13#@person1=Person("Dana", 42, "dana@example.com"), Person("Sam", 19, "sam@example.com")14person2→ Person(name='Bob', age=25, email='bob@example.com') = Person("Bob", 25, "bob@example.com")1516# __repr__ is auto-generated17print(person1Person(name='Alice', age=30, email='alice@example.com'))1819# __eq__ is auto-generated20print(person1Person(name='Alice', age=30, email='alice@example.com') == person2Person(name='Bob', age=25, email='bob@example.com')) # False21print(person1Person(name='Alice', age=30, email='alice@example.com') == Person(person1.nameAlice, person1.age30, person1.emailalice@example.com)) # TrueoutputPerson(name='Alice', age=30, email='alice@example.com')
person1 ← Person(name='Dana', age=42, email='dana@example.com')
6class Person:7 name(empty): str8 age(empty): int9 email(empty): str1011# Create instances - __init__ is auto-generated12person1→ Person(name='Dana', age=42, email='dana@example.com') = Person("Dana", 42, "dana@example.com")13person2→ Person(name='Bob', age=25, email='bob@example.com') = Person("Bob", 25, "bob@example.com")1415# __repr__ is auto-generated16print(person1Person(name='Dana', age=42, email='dana@example.com'))1718# __eq__ is auto-generated19print(person1Person(name='Dana', age=42, email='dana@example.com') == person2Person(name='Bob', age=25, email='bob@example.com')) # False20print(person1Person(name='Dana', age=42, email='dana@example.com') == Person(person1.nameDana, person1.age42, person1.emaildana@example.com)) # TrueoutputPerson(name='Dana', age=42, email='dana@example.com')
person1 ← Person(name='Sam', age=19, email='sam@example.com')
6class Person:7 name(empty): str8 age(empty): int9 email(empty): str1011# Create instances - __init__ is auto-generated12person1→ Person(name='Sam', age=19, email='sam@example.com') = Person("Sam", 19, "sam@example.com")13person2→ Person(name='Bob', age=25, email='bob@example.com') = Person("Bob", 25, "bob@example.com")1415# __repr__ is auto-generated16print(person1Person(name='Sam', age=19, email='sam@example.com'))1718# __eq__ is auto-generated19print(person1Person(name='Sam', age=19, email='sam@example.com') == person2Person(name='Bob', age=25, email='bob@example.com')) # False20print(person1Person(name='Sam', age=19, email='sam@example.com') == Person(person1.nameSam, person1.age19, person1.emailsam@example.com)) # TrueoutputPerson(name='Sam', age=19, email='sam@example.com')
Default Values
Fields can have default values, but fields with defaults must come after fields without.
# Dataclass with default values
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int = 0
in_stock: bool = True
category: str = "General"
# Use defaults
product1 = Product("Laptop", 999.99)
print(product1)
# Override defaults
product2 = Product("Mouse", 29.99, quantity=50, category="Electronics")
print(product2)
# Out of stock product
product3 = Product("Phone", 599.99, in_stock=False)
print(product3)
quantity ← (empty), in_stock ← (empty), category ← (empty), product1 ← Product(name='Laptop', price=999.99, quantity=0, in_stock=True, category='General')
6class Product:7 name(empty): str8 price(empty): float9 quantity→ (empty): int = 010 in_stock→ (empty): bool = TrueTrue11 category→ (empty): str = "General"1213# Use defaults14product1→ Product(name='Laptop', price=999.99, quantity=0, in_stock=True, category='General') = Product("Laptop", 999.99)15print(product1Product(name='Laptop', price=999.99, quantity=0, in_stock=True, category='General'))1617# Override defaults18product2→ Product(name='Mouse', price=29.99, quantity=50, in_stock=True, category='Electronics') = Product("Mouse", 29.99, quantity=50, category="Electronics")19print(product2Product(name='Mouse', price=29.99, quantity=50, in_stock=True, category='Electronics'))2021# Out of stock product22product3→ Product(name='Phone', price=599.99, quantity=0, in_stock=False, category='General') = Product("Phone", 599.99, in_stock=False)23print(product3Product(name='Phone', price=599.99, quantity=0, in_stock=False, category='General'))outputProduct(name='Laptop', price=999.99, quantity=0, in_stock=True, category='General') Product(name='Mouse', price=29.99, quantity=50, in_stock=True, category='Electronics') Product(name='Phone', price=599.99, quantity=0, in_stock=False, category='General')
Frozen Dataclass
Use frozen=True to create immutable instances that can be used as dictionary keys.
# Frozen dataclasses (immutable)
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
# Create point
point = Point(10.5, 20.3)
print(f"Point: ({point.x}, {point.y})")
# Cannot modify - will raise FrozenInstanceError
try:
point.x = 100
except AttributeError as e:
print(f"Error: {e}")
# Can use as dictionary keys (hashable)
points_dict = {
Point(0, 0): "origin",
Point(10, 10): "ten-ten",
Point(5, 5): "five-five"
}
print(points_dict[Point(0, 0)])
point ← Point(x=10.5, y=20.3)
6class Point:7 x(empty): float8 y(empty): float910# Create point11point→ Point(x=10.5, y=20.3) = Point(10.5, 20.3)12print(f"Point: ({point.x10.5}, {point.y20.3})")outputPoint: (10.5, 20.3)except AttributeError as e:
16 point.x = 10017except AttributeError as e:18 print(f"Error: {ecannot assign to field 'x'}")outputError: cannot assign to field 'x'points_dict ← {Point(x=0, y=0): 'origin', Point(x=10, y=10): 'ten-ten', Point(x=5, y=5): 'five-five'}
20# Can use as dictionary keys (hashable)21points_dict→ {Point(x=0, y=0): 'origin', Point(x=10, y=10): 'ten-ten', Point(x=5, y=5): 'five-five'} = {22 Point(0, 0): "origin",23 Point(10, 10): "ten-ten",24 Point(5, 5): "five-five"25}26print(points_dict{Point(x=0, y=0): 'origin', Point(x=10, y=10): 'ten-ten', Point(x=5, y=5): 'five-five'}[Point(0, 0)])outputorigin
Field Function
Use field() for advanced options like mutable defaults or computed fields.
# Using field() for mutable defaults
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
customer: str
items: list = field(default_factory=list)
total: float = 0.0
def add_item(self, item: str, price: float):
self.items.append(item)
self.total += price
# Each cart gets its own list
cart1 = ShoppingCart("Alice")
cart1.add_item("Apple", 1.50)
cart1.add_item("Banana", 0.75)
cart2 = ShoppingCart("Bob")
cart2.add_item("Orange", 2.00)
print(f"{cart1.customer}'s cart: {cart1.items}, Total: ${cart1.total:.2f}")
print(f"{cart2.customer}'s cart: {cart2.items}, Total: ${cart2.total:.2f}")
items ← (empty), total ← (empty), cart1 ← ShoppingCart(customer='Alice', items=[], total=0.0)
6class ShoppingCart:7 customer(empty): str8 items→ (empty): list = field(default_factory=list)9 total→ (empty): float = 0.010 11 def add_item(self, item: str, price: float):12 self.items.append(item)13 self.total += price1415# Each cart gets its own list16cart1→ ShoppingCart(customer='Alice', items=[], total=0.0) = ShoppingCart("Alice")17cart1ShoppingCart(customer='Alice', items=[], total=0.0).add_item("Apple", 1.50)18cart1.add_item("Banana", 0.75)self.items ← ['Apple'], self.total ← 1.5
pass 1 of 311def add_item(selfShoppingCart(customer='Alice', items=[], total=0.0), itemApple: str, price1.5: float):12 self.items→ ['Apple'].append(itemApple)13 self.total→ 1.5 += price1.5All 3 passes — pass 1 is the card above pass selfitempriceself.itemsself.total1 ShoppingCart(customer='Alice', items=[], total=0.0) Apple 1.5 [] → ['Apple'] 0.0 → 1.5 2 ShoppingCart(customer='Alice', items=['Apple'], total=1.5) Banana 0.75 ['Apple'] → ['Apple', 'Banana'] 1.5 → 2.25 3 ShoppingCart(customer='Bob', items=[], total=0.0) Orange 2.0 [] → ['Orange'] 0.0 → 2.0 cart1 ← ShoppingCart(customer='Alice', items=['Apple'], total=1.5)
16cart1 = ShoppingCart("Alice")17cart1→ ShoppingCart(customer='Alice', items=['Apple'], total=1.5).add_item("Apple", 1.50)18cart1ShoppingCart(customer='Alice', items=['Apple'], total=1.5).add_item("Banana", 0.75)cart1 ← ShoppingCart(customer='Alice', items=['Apple', 'Banana'], total=2.25)
17cart1.add_item("Apple", 1.50)18cart1→ ShoppingCart(customer='Alice', items=['Apple', 'Banana'], total=2.25).add_item("Banana", 0.75)1920cart2→ ShoppingCart(customer='Bob', items=[], total=0.0) = ShoppingCart("Bob")21cart2ShoppingCart(customer='Bob', items=[], total=0.0).add_item("Orange", 2.00)cart2 ← ShoppingCart(customer='Bob', items=['Orange'], total=2.0)
20cart2 = ShoppingCart("Bob")21cart2→ ShoppingCart(customer='Bob', items=['Orange'], total=2.0).add_item("Orange", 2.00)2223print(f"{cart1.customerAlice}'s cart: {cart1.items['Apple', 'Banana']}, Total: ${cart1.total2.25:.2f}")24print(f"{cart2.customerBob}'s cart: {cart2.items['Orange']}, Total: ${cart2.total2.0:.2f}")outputAlice's cart: ['Apple', 'Banana'], Total: $2.25 Bob's cart: ['Orange'], Total: $2.00
Comparison and Ordering
Use order=True to enable comparison operators based on field values.
# Dataclass with ordering
from dataclasses import dataclass, field
@dataclass(order=True)
class Score:
value: int
player: str = field(compare=False) # Don't use in comparison
# Create scores
scores = [
Score(85, "Alice"),
Score(92, "Bob"),
Score(78, "Charlie"),
Score(95, "David")
]
# Can compare
print(f"Bob's score > Alice's score: {scores[1] > scores[0]}")
# Can sort
sorted_scores = sorted(scores, reverse=True)
print("\nLeaderboard:")
for rank, score in enumerate(sorted_scores, 1):
print(f"{rank}. {score.player}: {score.value}")
player ← (empty), scores ← [Score(value=85, player='Alice'), Score(value=92, player='Bob'), Score(value=78, player='Charlie'), Score(value=95, player='David')]
6class Score:7 value(empty): int8 player→ (empty): str = field(compare=False) # Don't use in comparison910# Create scores11scores→ [Score(value=85, player='Alice'), Score(value=92, player='Bob'), Score(value=78, player='Charlie'), Score(value=95, player='David')] = [12 Score(85, "Alice"),13 Score(92, "Bob"),14 Score(78, "Charlie"),15 Score(95, "David")16]1718# Can compare19print(f"Bob's score > Alice's score: {scores[1]Score(value=92, player='Bob') > scores[0]Score(value=85, player='Alice')}")2021# Can sort22sorted_scores→ [Score(value=95, player='David'), Score(value=92, player='Bob'), Score(value=85, player='Alice'), Score(value=78, player='Charlie')] = sorted(scores[Score(value=85, player='Alice'), Score(value=92, player='Bob'), Score(value=78, player='Charlie'), Score(value=95, player='David')], reverse=True)23print("\nLeaderboard:")24for rank, score in enumerate(sorted_scores, 1):outputBob's score > Alice's score: True Leaderboard:for rank, score in enumerate(sorted_scores, 1):
pass 1 of 423print("\nLeaderboard:")24for rank1, scoreScore(value=95, player='David') in enumerate(sorted_scores[Score(value=95, player='David'), Score(value=92, player='Bob'), Score(value=85, player='Alice'), Score(value=78, player='Charlie')], 1):25 print(f"{rank1}. {score.playerDavid}: {score.value95}")output1. David: 95All 4 passes — pass 1 is the card above pass rankscorescore.playerscore.value1 1 Score(value=95, player='David') David 95 2 2 Score(value=92, player='Bob') Bob 92 3 3 Score(value=85, player='Alice') Alice 85 4 4 Score(value=78, player='Charlie') Charlie 78
@seealso namedtuple_intro "NamedTuples for immutable data" @seealso typing_intro "Type hints"
Exercise: practical.py
Create an AppConfig dataclass with feature toggles and configuration management methods