Skip to content

← Precise Type Hints with Literals and Generics step 7 of 26

Easy Primitives

Give Each Task an ID from a ClassVar Counter

A to-do app numbers its tasks 1, 2, 3 as you create them. The next number lives on the class, because every task shares it. Each task's tags live on the task itself. You write the Task dataclass, and a solve that reports what happened.

assert solve(["a", "b", "c"], ["low", "invalid", "high"], "urgent") == (
    [1, 2], ["name", "priority", "tags", "id"], ["urgent"], [], ["b"],
)

Task "b" has an invalid priority, so creating it raises ValueError and uses no ID. Task "c" gets ID 2.

ClassVar and two kinds of field

from dataclasses import dataclass, field, fields
from typing import ClassVar

@dataclass
class Task:
    PRIORITIES: ClassVar[tuple[str, ...]] = ("low", "normal", "high")
    _next_id: ClassVar[int] = 1

    name: str
    priority: str = "normal"
    tags: list[str] = field(default_factory=list)
    id: int = field(init=False, default=0)

    def __post_init__(self) -> None:
        ...
  • ClassVar marks an attribute of the class itself, shared by every instance. @dataclass leaves it out of the fields, so it is not a parameter of Task(...).
  • field(default_factory=list) calls list() for each new task, so every task gets its own empty list. @dataclass raises ValueError for a plain tags: list[str] = [], because all tasks would share that one list.
  • field(init=False, default=0) keeps id a field but leaves it out of the parameters of Task(...).
  • __post_init__ is a method that @dataclass calls at the end of the __init__ it writes for you.

A later track covers dataclasses in depth.

What to write

In __post_init__:

  • If self.priority is not exactly one of Task.PRIORITIES, raise ValueError. Matching is case-sensitive, and any message will do.
  • Only after that check passes, set self.id = Task._next_id, then add 1 to Task._next_id. A rejected task uses no ID.
  • Update the counter through Task, not self. self._next_id += 1 creates a new attribute on that one task and leaves the class counter unchanged. mypy reports it as Cannot assign to class variable "_next_id" via instance.
  • Names may repeat, and a name may be empty.

solve(names: list[str], priorities: list[str], tag: str) returns tuple[list[int], list[str], list[str], list[str], list[str]]:

  • First set Task._next_id = 1. The grader loads your code once and runs a batch of test cases against it. Without this reset, the IDs would continue from the previous case.
  • Pair names with priorities using zip(names, priorities, strict=True), and create a Task for each pair. If the lists differ in length, let zip raise its ValueError.
  • Catch the ValueError from each Task(...) call, record the rejected name, and continue.
  • Append tag once to the first task you created, if there is one.

The five lists are:

Position Contents
1 IDs of the created tasks, in input order
2 [f.name for f in fields(Task)]
3 The first created task's tags, or [] if there is none
4 The last created task's tags, or [] if there is none
5 Names of the rejected tasks, in input order

Position 2 is always ["name", "priority", "tags", "id"]. The two ClassVar attributes are missing because they are not fields.

Edge cases

  • With exactly one created task, the first and the last task are the same task, so positions 3 and 4 are both [tag].
  • With two or more, the last task's tags stay [].
  • With no created tasks, positions 1, 3 and 4 are [], and position 2 still lists the field names.
assert solve(["only"], ["normal"], "hot") == (
    [1], ["name", "priority", "tags", "id"], ["hot"], ["hot"], [],
)