1"""
2Python Dataclasses
3
4Demonstrates:
5- @dataclass decorator
6- field() with default values and factories
7- Post-initialization processing
8- Frozen dataclasses (immutable)
9- Inheritance with dataclasses
10- Comparison with NamedTuple and attrs
11- asdict() and astuple()
12- __post_init__ hook
13"""
14
15from dataclasses import dataclass, field, asdict, astuple, FrozenInstanceError
16from typing import List, Optional, ClassVar
17from collections import namedtuple
18
19
20def section(title: str) -> None:
21 """Print a section header."""
22 print("\n" + "=" * 60)
23 print(f" {title}")
24 print("=" * 60)
25
26
27# =============================================================================
28# Basic Dataclass
29# =============================================================================
30
31section("Basic Dataclass")
32
33
34@dataclass
35class Point:
36 """Simple 2D point."""
37 x: float
38 y: float
39
40
41p1 = Point(10, 20)
42p2 = Point(10, 20)
43p3 = Point(30, 40)
44
45print(f"p1: {p1}")
46print(f"p1.x = {p1.x}, p1.y = {p1.y}")
47
48# Automatic __eq__
49print(f"\np1 == p2: {p1 == p2}")
50print(f"p1 == p3: {p1 == p3}")
51
52# Automatic __repr__
53print(f"repr(p1): {repr(p1)}")
54
55
56# =============================================================================
57# Default Values
58# =============================================================================
59
60section("Default Values")
61
62
63@dataclass
64class Rectangle:
65 """Rectangle with default dimensions."""
66 width: float = 10.0
67 height: float = 5.0
68 color: str = "blue"
69
70
71rect1 = Rectangle()
72rect2 = Rectangle(width=20)
73rect3 = Rectangle(30, 15, "red")
74
75print(f"rect1: {rect1}")
76print(f"rect2: {rect2}")
77print(f"rect3: {rect3}")
78
79
80# =============================================================================
81# field() with default_factory
82# =============================================================================
83
84section("field() with default_factory")
85
86
87@dataclass
88class TodoList:
89 """Todo list with mutable default."""
90 owner: str
91 items: List[str] = field(default_factory=list) # Correct way for mutable defaults
92 tags: List[str] = field(default_factory=lambda: ["general"])
93
94
95todo1 = TodoList("Alice")
96todo2 = TodoList("Bob")
97
98todo1.items.append("Buy milk")
99todo2.items.append("Call dentist")
100
101print(f"todo1: {todo1}")
102print(f"todo2: {todo2}")
103print("Lists are separate (not shared)")
104
105
106# =============================================================================
107# field() Options
108# =============================================================================
109
110section("field() Options")
111
112
113@dataclass
114class Product:
115 """Product with various field options."""
116 name: str
117 price: float
118 quantity: int = 0
119
120 # Excluded from __repr__
121 _internal_id: str = field(default="", repr=False)
122
123 # Excluded from __init__
124 total_value: float = field(init=False, repr=True)
125
126 # Excluded from comparison
127 last_updated: str = field(default="", compare=False)
128
129 # Class variable (not instance field)
130 category: ClassVar[str] = "General"
131
132 def __post_init__(self):
133 """Calculate derived fields."""
134 self.total_value = self.price * self.quantity
135
136
137prod = Product("Widget", 19.99, 10, _internal_id="INTERNAL-123")
138print(f"Product: {prod}")
139print(f"total_value: {prod.total_value}")
140print(f"Category (class var): {Product.category}")
141
142
143# =============================================================================
144# __post_init__ Hook
145# =============================================================================
146
147section("__post_init__ Hook")
148
149
150@dataclass
151class User:
152 """User with validation and derived fields."""
153 username: str
154 email: str
155 age: int
156 full_name: str = field(init=False)
157
158 def __post_init__(self):
159 """Validate and initialize derived fields."""
160 # Validation
161 if self.age < 0:
162 raise ValueError("Age cannot be negative")
163 if "@" not in self.email:
164 raise ValueError("Invalid email")
165
166 # Derived field
167 self.full_name = f"User: {self.username}"
168
169
170user = User("alice", "alice@example.com", 30)
171print(f"User: {user}")
172print(f"full_name: {user.full_name}")
173
174try:
175 invalid_user = User("bob", "invalid-email", 25)
176except ValueError as e:
177 print(f"\nValidation error: {e}")
178
179
180# =============================================================================
181# Frozen Dataclasses (Immutable)
182# =============================================================================
183
184section("Frozen Dataclasses (Immutable)")
185
186
187@dataclass(frozen=True)
188class ImmutablePoint:
189 """Immutable point - cannot modify after creation."""
190 x: float
191 y: float
192
193 def move(self, dx: float, dy: float) -> 'ImmutablePoint':
194 """Return new point with offset."""
195 return ImmutablePoint(self.x + dx, self.y + dy)
196
197
198p = ImmutablePoint(10, 20)
199print(f"Original: {p}")
200
201p_moved = p.move(5, 3)
202print(f"Moved: {p_moved}")
203print(f"Original unchanged: {p}")
204
205try:
206 p.x = 100 # This will fail
207except FrozenInstanceError as e:
208 print(f"\nCannot modify frozen instance: {e}")
209
210
211# =============================================================================
212# Comparison and Ordering
213# =============================================================================
214
215section("Comparison and Ordering")
216
217
218@dataclass(order=True)
219class Version:
220 """Version with automatic ordering."""
221 major: int
222 minor: int
223 patch: int
224
225
226v1 = Version(1, 0, 0)
227v2 = Version(1, 5, 2)
228v3 = Version(2, 0, 0)
229
230print(f"v1: {v1}")
231print(f"v2: {v2}")
232print(f"v3: {v3}")
233print(f"\nv1 < v2: {v1 < v2}")
234print(f"v2 < v3: {v2 < v3}")
235print(f"sorted([v3, v1, v2]): {sorted([v3, v1, v2])}")
236
237
238# =============================================================================
239# Custom Ordering
240# =============================================================================
241
242section("Custom Ordering")
243
244
245@dataclass(order=True)
246class Person:
247 """Person with custom sort order."""
248 sort_index: int = field(init=False, repr=False)
249 name: str
250 age: int
251 salary: float
252
253 def __post_init__(self):
254 # Sort by salary (primary), then age (secondary)
255 self.sort_index = self.age # Can customize as needed
256
257
258people = [
259 Person("Alice", 30, 75000),
260 Person("Bob", 25, 60000),
261 Person("Charlie", 35, 90000),
262]
263
264print("Original:")
265for p in people:
266 print(f" {p.name}: age={p.age}, salary={p.salary}")
267
268print("\nSorted by age (via sort_index):")
269for p in sorted(people):
270 print(f" {p.name}: age={p.age}, salary={p.salary}")
271
272
273# =============================================================================
274# Inheritance
275# =============================================================================
276
277section("Inheritance with Dataclasses")
278
279
280@dataclass
281class Animal:
282 """Base animal class."""
283 name: str
284 age: int
285
286
287@dataclass
288class Dog(Animal):
289 """Dog extends Animal."""
290 breed: str
291 is_good_boy: bool = True
292
293
294dog = Dog("Buddy", 5, "Golden Retriever")
295print(f"Dog: {dog}")
296print(f"Is good boy? {dog.is_good_boy}")
297
298
299# =============================================================================
300# asdict() and astuple()
301# =============================================================================
302
303section("asdict() and astuple()")
304
305
306@dataclass
307class Book:
308 """Book with author information."""
309 title: str
310 author: str
311 year: int
312 pages: int
313
314
315book = Book("Python Tricks", "Dan Bader", 2017, 301)
316
317# Convert to dict
318book_dict = asdict(book)
319print(f"asdict(): {book_dict}")
320print(f"Type: {type(book_dict)}")
321
322# Convert to tuple
323book_tuple = astuple(book)
324print(f"\nastuple(): {book_tuple}")
325print(f"Type: {type(book_tuple)}")
326
327
328# =============================================================================
329# Nested Dataclasses
330# =============================================================================
331
332section("Nested Dataclasses")
333
334
335@dataclass
336class Address:
337 """Address information."""
338 street: str
339 city: str
340 zip_code: str
341
342
343@dataclass
344class Company:
345 """Company with address."""
346 name: str
347 address: Address
348 employees: int
349
350
351address = Address("123 Main St", "Springfield", "12345")
352company = Company("Acme Inc", address, 50)
353
354print(f"Company: {company}")
355
356# asdict with nested dataclasses
357company_dict = asdict(company)
358print(f"\nasdict() (nested): {company_dict}")
359
360
361# =============================================================================
362# Comparison with NamedTuple
363# =============================================================================
364
365section("Comparison with NamedTuple")
366
367# NamedTuple
368PersonTuple = namedtuple('PersonTuple', ['name', 'age'])
369
370
371@dataclass
372class PersonDataclass:
373 """Person as dataclass."""
374 name: str
375 age: int
376
377
378pt = PersonTuple("Alice", 30)
379pd = PersonDataclass("Alice", 30)
380
381print("NamedTuple:")
382print(f" Creation: {pt}")
383print(f" Immutable: {True}")
384print(f" Default values: Limited (via defaults)")
385print(f" Methods: Can't add methods to instance")
386
387print("\nDataclass:")
388print(f" Creation: {pd}")
389print(f" Immutable: {False} (unless frozen=True)")
390print(f" Default values: Full support with field()")
391print(f" Methods: Can add methods")
392
393
394# =============================================================================
395# Match Pattern (Python 3.10+)
396# =============================================================================
397
398section("Pattern Matching with Dataclasses")
399
400
401@dataclass
402class Circle:
403 """Circle shape."""
404 radius: float
405
406
407@dataclass
408class Rectangle:
409 """Rectangle shape."""
410 width: float
411 height: float
412
413
414def area(shape):
415 """Calculate area using pattern matching."""
416 match shape:
417 case Circle(radius=r):
418 return 3.14159 * r * r
419 case Rectangle(width=w, height=h):
420 return w * h
421 case _:
422 return 0
423
424
425circle = Circle(5)
426rect = Rectangle(4, 6)
427
428print(f"Circle area: {area(circle)}")
429print(f"Rectangle area: {area(rect)}")
430
431
432# =============================================================================
433# Comparison with attrs (Conceptual)
434# =============================================================================
435
436section("Dataclasses vs attrs")
437
438print("""
439Dataclasses (stdlib, Python 3.7+):
440 @dataclass
441 class Point:
442 x: int
443 y: int
444
445 Pros:
446 - Built into Python 3.7+
447 - No external dependencies
448 - Standard library support
449 - Good IDE integration
450
451 Cons:
452 - Less feature-rich than attrs
453 - No validators (need __post_init__)
454 - No converters
455
456attrs (third-party, more features):
457 @attrs.define
458 class Point:
459 x: int
460 y: int
461
462 Pros:
463 - More features (validators, converters)
464 - Works with Python 2.7+
465 - More mature
466 - Slots support
467
468 Cons:
469 - External dependency
470 - Additional package to maintain
471
472When to use:
473- Dataclasses: Default choice for Python 3.7+
474- attrs: Need advanced features or Python 2.7 support
475- NamedTuple: Need immutable, tuple-like behavior
476- Regular class: Need complex custom behavior
477""")
478
479
480# =============================================================================
481# Real-World Example
482# =============================================================================
483
484section("Real-World Example")
485
486
487@dataclass
488class BlogPost:
489 """Blog post with metadata."""
490 title: str
491 content: str
492 author: str
493 tags: List[str] = field(default_factory=list)
494 published: bool = False
495 views: int = 0
496 slug: str = field(init=False)
497
498 def __post_init__(self):
499 """Generate slug from title."""
500 self.slug = self.title.lower().replace(" ", "-")
501
502 def publish(self):
503 """Mark post as published."""
504 self.published = True
505
506 def increment_views(self):
507 """Increment view count."""
508 self.views += 1
509
510 def summary(self) -> str:
511 """Get post summary."""
512 return f"{self.title} by {self.author} ({self.views} views)"
513
514
515post = BlogPost(
516 title="Python Dataclasses Guide",
517 content="Lorem ipsum...",
518 author="Alice",
519 tags=["python", "tutorial"]
520)
521
522print(f"Post: {post}")
523print(f"Slug: {post.slug}")
524
525post.publish()
526post.increment_views()
527post.increment_views()
528
529print(f"Summary: {post.summary()}")
530print(f"Published: {post.published}")
531
532
533# =============================================================================
534# Summary
535# =============================================================================
536
537section("Summary")
538
539print("""
540Dataclass features:
5411. @dataclass decorator - auto-generate methods
5422. field() - customize field behavior
5433. default_factory - mutable defaults
5444. __post_init__ - post-initialization processing
5455. frozen=True - immutable instances
5466. order=True - comparison operators
5477. asdict()/astuple() - conversion utilities
5488. Inheritance - works naturally
5499. Pattern matching - structural pattern matching support
550
551Generated methods:
552- __init__ - initialization
553- __repr__ - string representation
554- __eq__ - equality comparison
555- __lt__, __le__, __gt__, __ge__ - ordering (if order=True)
556- __hash__ - hashing (if frozen=True)
557
558Use dataclasses when:
559- Need simple data containers
560- Want automatic __init__, __repr__, __eq__
561- Need type hints
562- Want clean, readable code
563- Python 3.7+ is available
564
565Avoid when:
566- Need complex custom initialization
567- Require validation beyond __post_init__
568- Need converters/validators (use attrs instead)
569""")