Learn by reading through in order

enum and dataclasses — Named Constants and Data Classes

Replace string literals with Enum, auto-number and sort with IntEnum + auto(), generate boilerplate via @dataclass, and use field(default_factory=list) through examples.

Two modules for making the meaning of values explicit. enum lets you replace string literals scattered through your code with named constants, so typos surface at runtime instead of silently passing through. dataclasses provides a way to define a "class that just holds data" in one line, eliminating the boilerplate of __init__ / __eq__ / __repr__.

Enum — replace string literals with named constants

When string literals like "paid" / "shipped" are scattered through your code, typos like "shippped" slip through until runtime, autocomplete doesn't help, and refactors are fragile. Enum solves this by defining the set of constants as a class — the names you define autocomplete in your editor, typos raise AttributeError immediately, iteration over all members works out of the box, and you get a value safe to use as a dict key or switch label.

# Status strings scattered through the code
if status == "paid":
    ship_order(order_id)
elif status == "shippped":   # Typo — should be "shipped", but you won't notice until runtime
    notify_shipped(order_id)
elif status == "cancelled":
    refund(order_id)
String literals vs. Enum
if status == "paid":(string literal)Typo "paied"raises no exception→ Bug stays hiddenif status == OrderStatus.PAID:(Enum)OrderStatus.PAEID→ AttributeError→ Caught immediately
Bare strings don't autocomplete and typos slip through until runtime. Enum only allows the names you defined, so you get autocomplete, typo detection, and full-member iteration in one go.
from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    CANCELLED = "cancelled"

# Member access: each member carries a name and a value
print(OrderStatus.PAID)            # OrderStatus.PAID
print(OrderStatus.PAID.name)       # 'PAID'
print(OrderStatus.PAID.value)      # 'paid'

# Compare with ==
status = OrderStatus.PAID
if status == OrderStatus.PAID:
    print("Payment received")

Define an order status as an Enum and read the members' info.

① Import Enum from the enum module

② Define an Enum class OrderStatus with the four members PENDING / PAID / SHIPPED / CANCELLED (values are the lowercase strings "pending" / "paid" / "shipped" / "cancelled")

③ Print OrderStatus.PAID.name as PAID name: ◯◯

④ Print OrderStatus.PAID.value as PAID value: ◯◯

⑤ Print all member names as All members: ◯ (you can iterate with for s in OrderStatus)

(If your code runs correctly, the explanation will appear.)

Python Editor

Run code to see output

Build a function that compares Enum members in if/elif and returns a per-status message. Branching on member-to-member equality instead of string literals is the safer way to write it.

① Define OrderStatus from Practice 1

② Define a get_message(status) function that uses if/elif over each member to return: PENDING → "Waiting for payment", PAID → "Preparing for shipment", SHIPPED → "Shipped", CANCELLED → "Cancelled"

③ Print get_message(OrderStatus.PAID) as PAID: ◯

④ Print get_message(OrderStatus.SHIPPED) as SHIPPED: ◯

Python Editor

Run code to see output

IntEnum and auto — auto-numbering and numeric comparison

IntEnum is an Enum that inherits from int, so members compare and arithmetic-operate as integers. It's the right pick for constants where numeric ordering matters — priorities, levels, ranks. auto() is a function that assigns values automatically in place of writing them by hand: line up auto() calls and they become 1, 2, 3, 4... in definition order.

When the specific numbers don't matter (only the order does), auto() is safer because adding or removing members doesn't require rewriting numbers by hand.

IntEnum + auto
class Priority(IntEnum): LOW = auto() MEDIUM = auto()auto() auto-numbers1, 2, 3Priority.LOW < Priority.HIGH→ True (int compare)
auto() auto-numbers 1, 2, 3, .... IntEnum compares with int, so Priority.LOW < Priority.HIGH works — numeric ordering encodes the priority.

Define a Priority IntEnum with auto() and sort multiple tasks from high priority to low. Confirm in a real-world scenario that auto() auto-numbers and that IntEnum can compare and sort as int.

① Import IntEnum and auto from enum

② Define a Priority IntEnum class with four members LOW / MEDIUM / HIGH / URGENT (values all auto())

③ Build a list of all member values and print as Numbering: ◯ (confirming auto() assigned 1, 2, 3, 4)

④ Sort the task list [("Cleaning", Priority.LOW), ("Email replies", Priority.HIGH), ("Prepare materials", Priority.URGENT), ("Review", Priority.MEDIUM)] from high priority to low, then under the heading Execution order: print each task in the form - name (priority)

Python Editor

Run code to see output

@dataclass — define a data-holding class in one line

Writing a "plain class with just fields" by hand means defining __init__ to set attributes, __eq__ (the dunder method called for == comparisons) for content equality, __repr__ (the dunder method called by print and the REPL) for a readable display, and so on — the same boilerplate patterns lining up every time. A single @dataclass decorator generates all of those automatically from your type-hinted field declarations.

In other words, just lining up type-hinted attributes builds the whole class — you get the same features for far less code than handwritten classes.

What @dataclass auto-generates
@dataclassclass Order: id: int customer: str→ Auto-generated__init__Initialize attributes__eq__== if all fields match__repr__Order(id=..., customer=...)
Just declare type-hinted fields and you get __init__ / __eq__ / __repr__ for free. field(default_factory=list) lets you safely set mutable defaults like an empty list, and frozen=True turns the class immutable (attribute changes blocked).

Use default_factory for mutable defaults

Trying to default a list with @dataclass by writing items: list = [] raises a SyntaxError (or deprecation warning). It's a guard against the classic bug of "every instance ends up sharing the same list". Use field(default_factory=list) instead, which creates a fresh empty list per instance, making it safe. Same idea for dict / set: field(default_factory=dict).

Define an Order with @dataclass and confirm the auto-generated __eq__ and __repr__.

① Import dataclass and field from dataclasses

② Define an Order class decorated with @dataclass — four fields: id: int, customer: str, items: list = field(default_factory=list), is_paid: bool = False

③ Build an instance with Order(id=1234, customer="Alice", items=["apple", "banana"]) and print it directly (the auto-generated __repr__ activates)

④ Build a second instance with the same content and print the result of comparing with == as Equal: True / False

⑤ Change the first instance's is_paid to True, then compare with == again and print as Equal after change: True / False

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What's the advantage of replacing "paid" string literal comparison with an Enum?

Q2What's the advantage of auto() in IntEnum?

Q3Which is the correct way to default a field to a list in @dataclass?