Build a Sprint Board with OOP Patterns

Design a Jira-like sprint board using OOP patterns in Python.

Introduction

30 Second Summary

Every software team uses tools like Jira to track tasks across sprints. When interviewers say "design a task management system," they're testing whether you can decompose that familiar workflow into classes, relationships, and behavior.

In this project, you will build a terminal-based Sprint Board Engine using Python. You'll create a multi-class system with enums, dataclasses, inheritance, and composition that demonstrates the exact OOP concepts interviewers test.

What You'll Build

A terminal-based Sprint Board Engine that models a simplified Jira-like sprint system with priority-sorted tasks, polymorphic subclasses, and status tracking.

By the end of this project, you'll have:

  • A Priority enum and Task dataclass with automatic priority-based sorting that requires zero custom comparison logic.
  • A class hierarchy with Story and Bug subclasses that behave polymorphically through a shared interface.
  • A Sprint container class that manages collections of tasks through composition, plus a StatusMixin for tracking task progress.
  • Secret Mission: Add a Strategy pattern for pluggable sort orders you can swap at runtime.

Are there any prerequisites?

You should be comfortable with Python basics like functions, loops, and data structures. Basic class syntax (defining __init__ and methods) is also helpful, but you do not need prior experience with design patterns or advanced OOP.

Set Up the Project and Build the Priority Enum

Every OOP interview question starts the same way: identify your entities, then define how they compare to each other. Before you write a single class, you need a way to express priority as a sortable value.

In this step, you will create your project file, set up the imports you need, and build a Priority enum that supports natural integer comparison. This tiny building block powers every sort operation in the rest of the project.

In this step, get ready to:
  • Verify Python 3.12 is accessible and create the project file.
  • Add the standard library imports the project will use.
  • Build a Priority IntEnum and confirm it supports comparison operators.
Verify Python 3.12 is accessible

This project uses Python's dataclasses, IntEnum, and ABC features that require Python 3.12 or higher.

  • Open the VS Code integrated terminal by pressing Ctrl+`.
  • Check your Python version by running this command:
python --version

✔️ I see version 3.12 or higher

You are good to go. Move on to creating the project file below.

ⓧ I see an older version

Your system has an older Python that will not support all the features this project uses. You need Python 3.12 or higher.

  • Download the latest Python 3.12 installer from python.org/downloads.
  • During installation, check the box that says Add python.exe to PATH.
  • Close and reopen your VS Code terminal after installation.
  • Run python --version again to confirm you now see 3.12 or higher.

ⓧ Command not found

Python is not installed or not on your system PATH.

  • Download the Python 3.12 installer from python.org/downloads.
  • During installation, check the box that says Add python.exe to PATH. This is critical.
  • Complete the installer with all default settings.
  • Close and reopen your VS Code terminal after installation.
  • Run python --version again to confirm the output shows 3.12 or higher.
Create the project file and add imports

The entire project lives in a single file. This keeps the focus on OOP concepts rather than file structure.

  • In the VS Code Explorer sidebar, click the New File icon (the page icon with a plus sign).
  • Name the file sprint_board.py and press Enter.
  • Add the following imports at the top of sprint_board.py:
from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod

What do these imports do?

  • dataclass and field let you define classes with automatic __init__ and comparison methods, cutting boilerplate.
  • IntEnum gives you named constants that behave as integers. You can compare them with >, <, and sort them naturally.
  • ABC and abstractmethod let you define abstract base classes that enforce a contract on subclasses. You will use these later for the Strategy pattern.
  • Save sprint_board.py.
  • Confirm the imports work by running this command in your terminal:
python sprint_board.py

You should see no output and no errors. The file runs silently because it only contains imports so far.

Seeing a ModuleNotFoundError?

All three modules (dataclasses, enum, abc) are part of Python's standard library. If you see an import error, confirm you are running with Python 3.12 and not an older system Python.

Help me fix my import error in sprint_board.py

Write the Priority IntEnum

Priority is the foundation of task sorting. By using IntEnum, each priority level is both a readable name and a sortable integer.

  • Below the imports in sprint_board.py, add the Priority enum and a test print statement:
class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


print(Priority.HIGH > Priority.LOW)

What does this code do?

Priority inherits from IntEnum, which means each member (LOW, MEDIUM, HIGH) is simultaneously a named constant and an integer.

The print statement tests that HIGH (3) is greater than LOW (1). If this prints True, your enum supports comparison operators. This is the foundation that lets sorted() order tasks by priority later.

  • Save sprint_board.py.
  • Run the file to confirm the comparison works:
python sprint_board.py

You should see True printed in your terminal. This confirms that IntEnum gives you integer-comparable named constants.

Why IntEnum instead of plain integers?

You could use 1, 2, 3 directly, but interviewers look for named constants. They show you know when to reach for an enum over magic numbers.

IntEnum specifically (as opposed to plain Enum) is the right choice here because you need the values to participate in integer comparisons and sorting.

Seeing False instead of True?

Make sure your print statement compares Priority.HIGH > Priority.LOW (HIGH first, LOW second). If the order is reversed, you will get False.

Help me debug my Priority IntEnum comparison

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is what your complete sprint_board.py should look like at this point:

from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


print(Priority.HIGH > Priority.LOW)

Your Priority enum is locked in and sortable. Next up, you will build the Task dataclass that uses this enum to sort itself automatically.

Define the Task Entity with Dataclass Ordering

Your Priority enum gives you named constants that Python can compare as integers. Now you need an entity that uses those priorities.

In this step, you will build the Task class using Python's dataclass decorator. By choosing the right field order and decorator options, you get automatic sorting by priority without writing any comparison logic yourself.

In this step, get ready to:
  • Create a Task dataclass with automatic ordering based on priority.
  • Test sorting a mixed list of tasks and confirm they print in priority order.
  • Understand why field declaration order is the key to making this work.
Create the Task dataclass

The @dataclass(order=True) decorator auto-generates comparison methods (__lt__, __le__, __gt__, __ge__) by comparing instances as tuples of their fields in declaration order. This means whatever field you put first controls how sorting works.

  • In sprint_board.py, find this line:
print(Priority.HIGH > Priority.LOW)
  • Delete that entire line. It was just a quick test to verify your enum. Replace it with the following Task class:
# @dataclass(order=True) compares instances as tuples of fields in declaration order.
# By putting priority first and marking other fields compare=False,
# we get sorting purely by priority with zero custom comparison code.
@dataclass(order=True)
class Task:
    priority: Priority
    title: str = field(compare=False)
    description: str = field(compare=False, default="")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name}"

    def __str__(self) -> str:
        return self.display()

What does this code do?

  • priority: Priority is declared first. Since order=True compares fields in declaration order, this is the only field Python uses for sorting.
  • field(compare=False) on title and description excludes them from the generated comparison methods entirely.
  • display() returns a formatted string like Login page : HIGH for clean output.
  • __str__ calls display() so that print(task) automatically uses your formatted output.

Why does field order matter?

@dataclass(order=True) compares instances as if they were tuples of their fields, in the order you declared them. By putting priority first and marking everything else compare=False, you get sorting purely by priority with zero custom comparison code.

This is exactly the kind of idiomatic Python that interviewers look for. You chose the right tool for the job instead of manually implementing __lt__.

You won't see a change yet. The next chunk adds test code that proves the sorting works.

Sort and display Task instances

To confirm that dataclass ordering works, you need a few Task instances with different priorities. Python's built-in sorted() function will use the comparison methods that @dataclass(order=True) generated for you.

  • Add the following test code at the bottom of sprint_board.py, below the Task class:
t1 = Task(Priority.HIGH, "High task")
t2 = Task(Priority.LOW, "Low task")
t3 = Task(Priority.MEDIUM, "Medium task")
for t in sorted([t1, t2, t3]):
    print(t.display())

What does this code do?

  • Three tasks are created with HIGH, LOW, and MEDIUM priorities (deliberately out of order).
  • sorted() uses the auto-generated __lt__ method to sort by the priority field (LOW=1 comes first, HIGH=3 comes last).
  • display() prints each task in the title : PRIORITY format.
  • Save sprint_board.py.
  • Run the file by entering this command in your terminal:
python sprint_board.py

You should see three lines printed in priority order:

Low task : LOW
Medium task : MEDIUM
High task : HIGH

Not seeing the sorted output?

  • Make sure you deleted the old print(Priority.HIGH > Priority.LOW) line. If it's still there, you'll see True printed before your tasks.
  • Check that your Task class has @dataclass(order=True) on the line directly above class Task:. Without order=True, Python raises a TypeError when you try to sort.
  • Verify that priority is the first field declared in your class. If title comes first, tasks sort alphabetically instead of by priority.

Help me debug my Task dataclass sorting

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here is the complete sprint_board.py at this point:

from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


# @dataclass(order=True) compares instances as tuples of fields in declaration order.
# By putting priority first and marking other fields compare=False,
# we get sorting purely by priority with zero custom comparison code.
@dataclass(order=True)
class Task:
    priority: Priority
    title: str = field(compare=False)
    description: str = field(compare=False, default="")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name}"

    def __str__(self) -> str:
        return self.display()


t1 = Task(Priority.HIGH, "High task")
t2 = Task(Priority.LOW, "Low task")
t3 = Task(Priority.MEDIUM, "Medium task")
for t in sorted([t1, t2, t3]):
    print(t.display())

You now have a Task entity that sorts itself automatically. Next up, you will extend this into a class hierarchy with Story and Bug subclasses that each display differently while still sorting the same way.

Add Inheritance with Story and Bug Subclasses

Your Task dataclass sorts itself by priority automatically. But real sprint boards don't just have generic tasks. They have user stories with point estimates and bugs with severity levels.

In this step, you'll use inheritance to create specialized Story and Bug subclasses that add their own fields and override display behavior. The sorting still works because they inherit the comparison logic from Task.

In this step, get ready to:
  • Create Story and Bug subclasses that extend Task with type-specific fields.
  • Test polymorphism by sorting a mixed list of different task types.
  • Understand why print() works identically for any task type.
Create the Story subclass

A Story represents a user-facing feature with a point estimate. It inherits all of Task's fields and sorting behavior, then adds its own.

  • In sprint_board.py, delete the three-line comment block that sits above @dataclass(order=True) on the Task class.
  • Delete the test code at the bottom of the file (the t1, t2, t3 variables and the for loop).
  • After the closing line of the Task class, add the Story subclass by copying this code:
@dataclass(order=True)
class Story(Task):
    points: int = field(compare=False, default=1)

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt]"

What does this code do?

  • class Story(Task) means Story inherits every field and method from Task (priority, title, description, sorting).
  • points: int = field(compare=False, default=1) adds a new field that doesn't affect sort order.
  • The display() override returns a format that includes the point estimate in square brackets.
  • Save sprint_board.py.

No visible output yet. The next chunk adds Bug and then you'll test both together.

Create the Bug subclass

A Bug represents a defect with a severity level instead of story points.

  • Directly below the Story class, add the Bug subclass by copying this code:
@dataclass(order=True)
class Bug(Task):
    severity: str = field(compare=False, default="minor")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity})"

What does this code do?

  • Bug follows the same pattern as Story. It inherits from Task and adds severity: str with a default of "minor".
  • The display() override formats severity in parentheses, giving Bug its own distinct output.
  • Both subclasses use @dataclass(order=True) and mark their new fields with compare=False, so sorting still works purely on the inherited priority field.
  • Save sprint_board.py.

No output yet. The next chunk wires everything together with a test.

Seeing a TypeError about field ordering?

Dataclass inheritance requires fields with defaults to come after fields without defaults. Since points and severity both have defaults, they work correctly after the inherited priority field (which has no default). Make sure you included default=1 and default="minor" in your field definitions.

Help me fix a dataclass field ordering error in my Story or Bug subclass.

Test polymorphism with a mixed list

Polymorphism means different types respond to the same interface in their own way. Here, sorted() and print() work identically on Task, Story, and Bug because they share the same comparison and display interface.

  • At the bottom of sprint_board.py, add this test code:
mixed = [
    Story(Priority.HIGH, "User auth", points=5),
    Bug(Priority.LOW, "Typo in footer", severity="minor"),
    Task(Priority.MEDIUM, "Write docs"),
]
for item in sorted(mixed):
    print(item)

What does this code do?

  • The mixed list contains three different types: Story, Bug, and plain Task.
  • sorted() works on all of them because they all inherit the same comparison methods from Task's @dataclass(order=True).
  • print(item) calls __str__() on each object, which delegates to display(). Each subclass's display() override produces its own format.
  • Save sprint_board.py.
  • Run the file by running:
python sprint_board.py
  • You should see three lines printed in priority order: the Bug (LOW) first, then the Task (MEDIUM), then the Story (HIGH). Each line uses its type-specific format.

Tasks not printing in the right order?

Check that priority is the first field declared in your Task class. The @dataclass(order=True) decorator compares fields in declaration order, so if title comes first, it will sort alphabetically instead of by priority.

Help me debug why my sorted task list isn't ordering by priority.

Understand why print() works for every task type

Notice that the test code uses print(item) instead of print(item.display()). This works because of the __str__ method on the base Task class.

Why does this matter for interviews?

The __str__ method on Task calls self.display(). When a Story or Bug instance calls __str__, Python resolves self.display() to the subclass's overridden version. This is polymorphism in action.

This means the Sprint class you'll build next can call print(task) on any task without knowing whether it's a Story, Bug, or plain Task. The correct format appears automatically. Interviewers love seeing this: you designed a system where new task types can be added without changing existing code.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


@dataclass(order=True)
class Task:
    priority: Priority
    title: str = field(compare=False)
    description: str = field(compare=False, default="")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name}"

    def __str__(self) -> str:
        return self.display()


@dataclass(order=True)
class Story(Task):
    points: int = field(compare=False, default=1)

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt]"


@dataclass(order=True)
class Bug(Task):
    severity: str = field(compare=False, default="minor")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity})"


mixed = [
    Story(Priority.HIGH, "User auth", points=5),
    Bug(Priority.LOW, "Typo in footer", severity="minor"),
    Task(Priority.MEDIUM, "Write docs"),
]
for item in sorted(mixed):
    print(item)

You now have three task types that sort and display themselves correctly. Next up, you'll build a Sprint container that holds and manages collections of these tasks using composition.

Build the Sprint Container with Composition

Your class hierarchy is working. Different task types share the same comparison interface, sort identically, yet display their own unique information.

Now you need something to hold and manage those tasks. In interview terms, this is composition: a Sprint HAS tasks, it is not a task. This "has-a" relationship is the most common way real systems organize collections of objects.

In this step, get ready to:
  • Create a Sprint class that owns a collection of tasks.
  • Add methods to display and filter the task collection.
  • Wire the full system together with a test script.
Create the Sprint class

The Sprint class is a plain Python class, not a dataclass. It manages a private list of tasks internally and exposes methods to interact with that list.

  • In sprint_board.py, find the test code at the bottom of the file (the mixed list and the for loop):
mixed = [
    Story(Priority.HIGH, "User auth", points=5),
    Bug(Priority.LOW, "Typo in footer", severity="minor"),
    Task(Priority.MEDIUM, "Write docs"),
]
for item in sorted(mixed):
    print(item)
  • Delete all of those lines. You will replace them with the Sprint class.
  • In their place, add the Sprint class skeleton by copying the following code:
class Sprint:
    def __init__(self, name: str):
        # Sprint owns a collection of tasks (composition)
        self.name = name
        self._tasks: list[Task] = []

    def add_task(self, task: Task) -> None:
        # Accept any Task subtype through the base interface
        self._tasks.append(task)

What does this code do?

This is a regular class (not a dataclass) because Sprint does not need automatic comparison or ordering. It is a container, not a data record.

  • self._tasks uses a leading underscore to signal "private". External code should use add_task() instead of touching the list directly.
  • add_task() accepts any Task (including Story and Bug) because they share the same base type. The Sprint never needs to know the concrete subclass.

What is composition?

Composition means one object owns and manages other objects. A Sprint HAS tasks. It is not a subclass of Task. Interviewers look for this distinction because it signals you understand when to use inheritance ("is-a") versus composition ("has-a").

  • Save sprint_board.py.
  • Run the file to confirm there are no syntax errors by running:
python sprint_board.py

The file should run without any output or errors. The Sprint class is defined but nothing calls it yet.

Seeing a syntax error?

  • Make sure you deleted all of the old test code (the mixed list and the for loop). Leftover fragments can cause indentation or name errors.
  • Check that the Sprint class starts at column 0 (no leading spaces). It should be at the same indentation level as your other class definitions.

Help me debug my Sprint class syntax error.

Add display and filter methods

The Sprint class needs two capabilities: displaying all tasks in priority order, and filtering tasks by a specific priority level.

  • In sprint_board.py, add the following two methods inside the Sprint class, below add_task:
    def display_board(self) -> None:
        # Print a formatted header with the sprint name
        print(f"\n{'=' * 40}")
        print(f"  Sprint: {self.name}")
        print(f"{'=' * 40}")
        # sorted() uses Task's dataclass ordering (by priority)
        for task in sorted(self._tasks):
            print(f"  {task}")
        print()

    def get_by_priority(self, priority: Priority) -> list[Task]:
        # Filter tasks matching the given priority level
        return [t for t in self._tasks if t.priority == priority]

What does this code do?

  • display_board() prints a header line, then iterates over sorted(self._tasks). Because every task is a dataclass with order=True, Python sorts them by priority automatically.
  • get_by_priority() uses a list comprehension to return only tasks matching a given priority. The Sprint class handles filtering without knowing whether a task is a Story, Bug, or plain Task.
  • Save sprint_board.py and run it again:
python sprint_board.py

No output or errors means the methods are syntactically correct. The next substep will call them and produce visible results.

Getting an indentation error?

  • Both methods must be indented one level inside the Sprint class (4 spaces from the left margin). They sit at the same indentation level as add_task.
  • Make sure there is no extra blank line between the class definition and the first method. A single blank line between methods is fine.

Help me fix my indentation error in Sprint.

Wire the system together

The Sprint class is ready. Now you will create a sprint, populate it with mixed task types, and call both methods to see composition and polymorphism working together.

  • At the bottom of sprint_board.py (below the Sprint class), add the following test block:
if __name__ == "__main__":
    # Create a sprint and populate it with mixed task types
    sprint = Sprint("Sprint 1 - MVP")
    sprint.add_task(Story(Priority.HIGH, "User authentication", points=5))
    sprint.add_task(Bug(Priority.LOW, "Typo in footer", severity="minor"))
    sprint.add_task(Task(Priority.MEDIUM, "Write documentation"))
    sprint.add_task(Bug(Priority.HIGH, "Login crash", severity="critical"))

    # Display all tasks sorted by priority
    sprint.display_board()

    # Filter and display only HIGH priority tasks
    print("HIGH priority tasks:")
    for task in sprint.get_by_priority(Priority.HIGH):
        print(f"  {task}")

What does this code do?

  • if __name__ == "__main__": ensures this test code only runs when you execute the file directly. It will not run if another file imports your classes later.
  • You add a mix of Story, Bug, and plain Task instances. The Sprint accepts all of them through the Task interface without knowing their concrete types.
  • display_board() sorts and prints every task. get_by_priority(Priority.HIGH) filters down to only high-priority items.
  • Save sprint_board.py.
  • Run the file to see your Sprint board in action:
python sprint_board.py

You should see a formatted sprint board with all tasks sorted by priority (LOW first, HIGH last), followed by a filtered list of only the HIGH priority tasks.

What should I see?

The board prints tasks in ascending priority order. LOW tasks appear first, MEDIUM tasks in the middle, and HIGH tasks last. Each task uses its own display() format (Story shows points, Bug shows severity).

The filtered section at the bottom shows only the two HIGH priority tasks. This confirms get_by_priority() works correctly.

Seeing a NameError or empty board?

  • If you see NameError: name 'Sprint' is not defined, make sure the if __name__ block is below the Sprint class, not inside it.
  • If the board prints but shows no tasks, check that your add_task method appends to self._tasks (with the underscore).

Help me debug my Sprint board output.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the full sprint_board.py file at this point:

from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


@dataclass(order=True)
class Task:
    priority: Priority
    title: str = field(compare=False)
    description: str = field(compare=False, default="")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name}"

    def __str__(self) -> str:
        return self.display()


@dataclass(order=True)
class Story(Task):
    points: int = field(compare=False, default=1)

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt]"


@dataclass(order=True)
class Bug(Task):
    severity: str = field(compare=False, default="minor")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity})"


class Sprint:
    def __init__(self, name: str):
        self.name = name
        self._tasks: list[Task] = []

    def add_task(self, task: Task) -> None:
        self._tasks.append(task)

    def display_board(self) -> None:
        print(f"\n{'=' * 40}")
        print(f"  Sprint: {self.name}")
        print(f"{'=' * 40}")
        for task in sorted(self._tasks):
            print(f"  {task}")
        print()

    def get_by_priority(self, priority: Priority) -> list[Task]:
        return [t for t in self._tasks if t.priority == priority]


if __name__ == "__main__":
    sprint = Sprint("Sprint 1 - MVP")
    sprint.add_task(Story(Priority.HIGH, "User authentication", points=5))
    sprint.add_task(Bug(Priority.LOW, "Typo in footer", severity="minor"))
    sprint.add_task(Task(Priority.MEDIUM, "Write documentation"))
    sprint.add_task(Bug(Priority.HIGH, "Login crash", severity="critical"))
    sprint.display_board()
    print("HIGH priority tasks:")
    for task in sprint.get_by_priority(Priority.HIGH):
        print(f"  {task}")

Your Sprint board is working. You have a container that manages tasks through the base interface, sorts them automatically, and filters by priority. Next up, you will layer status tracking onto your tasks using a mixin, so tasks can transition through TODO, IN_PROGRESS, and DONE.

Layer Status Management with a Mixin

Your Sprint container now manages collections of tasks through the Task interface. Every task sorts by priority and displays its type-specific format. But real sprint boards track something else: where each task is in the workflow.

In this step, you will add status tracking (TODO, IN_PROGRESS, DONE) to your task types using a mixin. A mixin is a class that adds behavior to other classes without becoming a parent in the main hierarchy. Status tracking is orthogonal to task type. A Story can exist without status, and status tracking can apply to any task type. This separation of concerns is exactly the reasoning interviewers want to hear.

In this step, get ready to:
  • Create a Status enum and a StatusMixin class that enforces forward-only transitions.
  • Build TrackedStory and TrackedBug using multiple inheritance.
  • Test the full flow with status transitions and status-based filtering.
Create the Status enum and StatusMixin class

Just like Priority gives named constants for importance levels, you need named constants for workflow states. The Status enum will use IntEnum again so you can compare statuses with <= to enforce forward-only transitions.

  • In sprint_board.py, find the Priority class and add the following Status enum directly below it (before the Task class):
class Status(IntEnum):
    TODO = 1
    IN_PROGRESS = 2
    DONE = 3

What does this code do?

Status defines three workflow states as integers. Because it uses IntEnum, you can compare values with <=. This makes enforcing forward-only transitions a single comparison.

You won't see a change yet. The next chunk (StatusMixin) uses this enum.

Now for the mixin itself. The StatusMixin hooks into the dataclass initialization process via __post_init__ to set a default status, then provides a property and a guarded transition method.

  • Add the StatusMixin class below the Bug class (before the Sprint class):
class StatusMixin:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

    def __post_init__(self):
        super().__post_init__()
        self._status = Status.TODO

    @property
    def status(self) -> Status:
        return self._status

    def transition_to(self, new_status: Status) -> None:
        if new_status <= self._status:
            raise ValueError(
                f"Cannot transition from {self._status.name} to {new_status.name}. "
                f"Status can only move forward."
            )
        self._status = new_status

What does this code do?

  • __post_init__ is called automatically after a dataclass finishes its own __init__. The mixin uses this hook to set self._status = Status.TODO without interfering with the dataclass fields.
  • The status property exposes the private _status attribute as read-only. External code can read status but can only change it through transition_to.
  • transition_to compares the new status against the current one using <=. Because Status is an IntEnum, this blocks any backward move (or re-assignment to the same state).
  • __init_subclass__ ensures the mixin plays nicely with Python's method resolution order (MRO) when combined with dataclass inheritance.

You won't see a change yet. The next chunk creates subclasses that use this mixin.

Why a mixin instead of adding status directly to Task?

Status tracking is a separate concern from what a task IS. Some tasks (like a plain documentation task in early planning) might not need status tracking at all.

By keeping it in a mixin, you can add status to any task type without modifying the existing hierarchy. This is the kind of design reasoning that scores highly in interviews.

Build TrackedStory and TrackedBug with multiple inheritance

Now you will combine the mixin with your existing subclasses. Multiple inheritance lets a class pull behavior from more than one parent.

  • Add the TrackedStory and TrackedBug classes directly below the StatusMixin class (before the Sprint class):
@dataclass(order=True)
class TrackedStory(StatusMixin, Story):
    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt] | {self.status.name}"


@dataclass(order=True)
class TrackedBug(StatusMixin, Bug):
    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity}) | {self.status.name}"

What does this code do?

  • TrackedStory(StatusMixin, Story) inherits from both StatusMixin and Story. It gets status tracking from the mixin and task fields from Story.
  • Each class overrides display() to append the current status name. This is polymorphism: calling display() on any task produces the right output for its type.
  • The mixin is listed FIRST in the inheritance tuple. This matters for Python's MRO. It ensures __post_init__ from the mixin runs during initialization.

You won't see a change yet. The next chunk wires everything together with test code.

Wire up the full flow and add status filtering

The Sprint class currently has no way to filter tasks by status. You will add a get_by_status method, then rewrite the test block to exercise status transitions.

  • In the Sprint class, add the following method below get_by_priority:
    def get_by_status(self, status: Status) -> list[Task]:
        return [
            t for t in self._tasks
            if hasattr(t, "status") and t.status == status
        ]

What does this code do?

This method filters tasks by workflow status. The hasattr(t, "status") check is important because the Sprint can hold both tracked tasks (with status) and plain Task instances (without status). Without this check, calling t.status on a plain Task would raise an AttributeError.

You won't see a change yet. The next chunk replaces your test code to exercise all the new features.

  • Replace your entire existing if __name__ == "__main__": block at the bottom of the file with this updated test code:
if __name__ == "__main__":
    sprint = Sprint("Sprint 1 - MVP")

    story1 = TrackedStory(Priority.HIGH, "User authentication", points=5)
    story2 = TrackedStory(Priority.MEDIUM, "Dashboard layout", points=3)
    bug1 = TrackedBug(Priority.HIGH, "Login crash on empty email", severity="critical")
    bug2 = TrackedBug(Priority.LOW, "Typo in footer", severity="minor")
    task1 = Task(Priority.MEDIUM, "Write documentation")

    # Transition some tasks forward through the workflow
    story1.transition_to(Status.IN_PROGRESS)
    bug1.transition_to(Status.IN_PROGRESS)
    bug1.transition_to(Status.DONE)

    sprint.add_task(story1)
    sprint.add_task(story2)
    sprint.add_task(bug1)
    sprint.add_task(bug2)
    sprint.add_task(task1)

    sprint.display_board()

    print("HIGH priority tasks:")
    for task in sprint.get_by_priority(Priority.HIGH):
        print(f"  {task}")

    print("\nIN_PROGRESS tasks:")
    for task in sprint.get_by_status(Status.IN_PROGRESS):
        print(f"  {task}")

    print("\nTesting invalid transition:")
    try:
        story1.transition_to(Status.TODO)
    except ValueError as e:
        print(f"  Caught: {e}")

What does this code do?

  • Creates a mix of TrackedStory, TrackedBug, and plain Task instances. The plain Task proves the Sprint can hold both tracked and untracked items.
  • Transitions story1 to IN_PROGRESS and bug1 all the way to DONE. These are valid forward moves.
  • Filters by priority (should return both HIGH tasks) and by status (should return only the IN_PROGRESS task).
  • Attempts a backward transition (story1.transition_to(Status.TODO)) which should raise a ValueError that the try/except block catches and prints.
  • Save sprint_board.py.
  • Run your file by running:
python sprint_board.py
  • You should see the sprint board with status labels (like | IN_PROGRESS and | DONE) appended to tracked tasks.
  • The IN_PROGRESS filter should show only story1 (User authentication).
  • The last line should read Caught: Cannot transition from IN_PROGRESS to TODO. Status can only move forward.

Seeing an AttributeError or TypeError?

  • If you see an error about __post_init__, make sure your TrackedStory and TrackedBug classes have the @dataclass(order=True) decorator. Without it, the dataclass machinery that calls __post_init__ does not activate.
  • Check the order of parents in your class definition. StatusMixin must come BEFORE Story or Bug in the inheritance tuple.
  • Make sure the Status enum is defined ABOVE StatusMixin in the file. Python reads top to bottom, so StatusMixin needs Status to already exist when it references Status.TODO.

Help me debug my StatusMixin or TrackedStory/TrackedBug classes

✔️ Awesome, I've got everything!

Great. Double check you have saved sprint_board.py and your output shows tasks with status labels, a successful filter, and a caught ValueError.

ⓧ I'd like to double check the full code

Here is the complete sprint_board.py file at this point:

from dataclasses import dataclass, field
from enum import IntEnum
from abc import ABC, abstractmethod


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


class Status(IntEnum):
    TODO = 1
    IN_PROGRESS = 2
    DONE = 3


@dataclass(order=True)
class Task:
    priority: Priority
    title: str = field(compare=False)
    description: str = field(compare=False, default="")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name}"

    def __str__(self) -> str:
        return self.display()


@dataclass(order=True)
class Story(Task):
    points: int = field(compare=False, default=1)

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt]"


@dataclass(order=True)
class Bug(Task):
    severity: str = field(compare=False, default="minor")

    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity})"


class StatusMixin:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

    def __post_init__(self):
        super().__post_init__()
        self._status = Status.TODO

    @property
    def status(self) -> Status:
        return self._status

    def transition_to(self, new_status: Status) -> None:
        if new_status <= self._status:
            raise ValueError(
                f"Cannot transition from {self._status.name} to {new_status.name}. "
                f"Status can only move forward."
            )
        self._status = new_status


@dataclass(order=True)
class TrackedStory(StatusMixin, Story):
    def display(self) -> str:
        return f"{self.title} : {self.priority.name} [{self.points}pt] | {self.status.name}"


@dataclass(order=True)
class TrackedBug(StatusMixin, Bug):
    def display(self) -> str:
        return f"{self.title} : {self.priority.name} (severity: {self.severity}) | {self.status.name}"


class Sprint:
    def __init__(self, name: str):
        self.name = name
        self._tasks: list[Task] = []

    def add_task(self, task: Task) -> None:
        self._tasks.append(task)

    def display_board(self) -> None:
        print(f"\n{'=' * 40}")
        print(f"  Sprint: {self.name}")
        print(f"{'=' * 40}")
        for task in sorted(self._tasks):
            print(f"  {task}")
        print()

    def get_by_priority(self, priority: Priority) -> list[Task]:
        return [t for t in self._tasks if t.priority == priority]

    def get_by_status(self, status: Status) -> list[Task]:
        return [
            t for t in self._tasks
            if hasattr(t, "status") and t.status == status
        ]


if __name__ == "__main__":
    sprint = Sprint("Sprint 1 - MVP")

    story1 = TrackedStory(Priority.HIGH, "User authentication", points=5)
    story2 = TrackedStory(Priority.MEDIUM, "Dashboard layout", points=3)
    bug1 = TrackedBug(Priority.HIGH, "Login crash on empty email", severity="critical")
    bug2 = TrackedBug(Priority.LOW, "Typo in footer", severity="minor")
    task1 = Task(Priority.MEDIUM, "Write documentation")

    story1.transition_to(Status.IN_PROGRESS)
    bug1.transition_to(Status.IN_PROGRESS)
    bug1.transition_to(Status.DONE)

    sprint.add_task(story1)
    sprint.add_task(story2)
    sprint.add_task(bug1)
    sprint.add_task(bug2)
    sprint.add_task(task1)

    sprint.display_board()

    print("HIGH priority tasks:")
    for task in sprint.get_by_priority(Priority.HIGH):
        print(f"  {task}")

    print("\nIN_PROGRESS tasks:")
    for task in sprint.get_by_status(Status.IN_PROGRESS):
        print(f"  {task}")

    print("\nTesting invalid transition:")
    try:
        story1.transition_to(Status.TODO)
    except ValueError as e:
        print(f"  Caught: {e}")

Secret mission

Add Pluggable Sort Strategies

Your Sprint board currently sorts by priority only. What if a team lead wants to sort by status to find TODO items, or alphabetically to locate a specific task? The Strategy pattern lets you swap sort algorithms at runtime without touching the Sprint class. This is the Open/Closed Principle in action.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using only the Python standard library. There are no cloud resources, no paid services, and no ongoing costs.

Resources you used:

  • sprint_board.py file in your project folder on the Desktop.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your Sprint Board Engine or extend it further.

  • Your sprint_board.py file stays on your Desktop. There are no running processes or background services to worry about.

Pause - I'll come back to this later

Since this project has no running processes or cloud resources, pausing is the same as keeping. Your file sits idle on disk until you return.

  • No action required. Your sprint_board.py file uses negligible disk space and has no background activity.

Delete - I don't want to use this again

Remove your project folder and file to start fresh.

  • Open a terminal in VS Code.
  • Navigate to your Desktop and delete the project folder that contains sprint_board.py by running:
cd ~/Desktop
rm sprint_board.py

On Windows?

If you created a project folder, use File Explorer to navigate to your Desktop, right-click the folder, and select Delete.

Nice Work!

Nice Work!

Nice work! You just built a fully functional Sprint Board Engine that demonstrates every core OOP concept interviewers test.

You've learned how to:

  • Built a Priority enum and Task dataclass with automatic ordering via @dataclass(order=True) and field declaration order.
  • Implemented Story and Bug subclasses demonstrating inheritance and polymorphism, plus a StatusMixin for encapsulated state transitions via multiple inheritance.
  • Composed a Sprint container that manages tasks through the Task interface, demonstrating composition over inheritance.
  • (Secret Mission) Implemented the Strategy pattern with pluggable sort algorithms, demonstrating the Open/Closed Principle and runtime behavior swapping.

Ready to quiz yourself?