Build a Task Dependency Graph Solver

Implement topological sort and cycle detection for a task pipeline CLI.

Introduction

30 Second Summary

Every time you install software, something behind the scenes figures out which pieces need to come first. Get the order wrong and the whole thing breaks.

In this project, you will build a Python command-line tool that takes a set of tasks with dependencies and computes the correct execution order. When a circular dependency makes ordering impossible, your tool catches it and names the exact tasks forming the loop.

What You'll Build

Run your finished solver on a build pipeline config and it prints the exact execution order, catches any circular dependencies by name, and generates a flowchart diagram you can view right inside VS Code.

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

  • A working topological sort that reads task definitions from a simple text file and prints a valid execution order to your terminal.
  • Cycle detection that catches circular dependencies and tells you exactly which tasks form the loop (e.g., "auth -> cache -> db -> auth").
  • A generated Mermaid diagram that renders your dependency graph as a visual flowchart in VS Code's built-in Markdown preview.
  • Secret Mission: Group independent tasks into parallel execution layers and validate your results against Python's built-in graphlib.TopologicalSorter.

Are there any prerequisites?

This project assumes you're comfortable with Python basics: functions, dictionaries, lists, and classes. No prior experience with graph theory or algorithms is needed.

Before We Start

Before you write any code, take a moment to lock in what you're about to build and why it matters.

Set Up Your Project

Before you write any graph algorithms, you need a clean project structure. A dedicated folder with separate files for your graph logic, CLI entry point, and visualization keeps the code organized as the solver grows.

In this step, you'll confirm your Python version, create the project folder, and set up the files you'll build on throughout this project.

In this step, get ready to:
  • Verify your Python environment.
  • Create the project folder and empty Python files.
  • Create a sample input file with test data.
Open VS Code and verify Python

You need a terminal inside VS Code to run all the commands in this project. Let's open the editor and confirm Python is ready.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (the backtick key, usually above Tab). You can also click View in the menu bar, then click Terminal.

This project requires Python 3.9 or higher. Confirm your version by running this command in the terminal:

python3 --version

You should see output like Python 3.9.x or higher (e.g. Python 3.11.4). Any version from 3.9 onward works.

Seeing command not found or a version below 3.9?

If your terminal says command not found, Python may not be on your system PATH. Try running python --version (without the 3) to check if Python is available under a different name.

If you see a version below 3.9, download the latest Python from python.org/downloads and install it. After installing, close and reopen your terminal, then try python3 --version again.

Help me figure out why python3 --version is not showing 3.9 or higher.

Create the project folder and files

Your solver needs three Python files: one for the graph data structure, one for the CLI, and one for visualization. Let's create the folder and all the files together.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir dep-graph-solver
cd dep-graph-solver
  • Create the three Python files by running this command:
touch graph.py main.py visualize.py
  • Confirm the files exist by running:
ls

You should see graph.py, main.py, and visualize.py listed in the output.

What are these files for?

  • graph.py holds the graph data structure and sorting algorithms.
  • main.py is the CLI entry point that parses input and prints results.
  • visualize.py generates Mermaid diagrams from your graph.

Don't see the files?

Make sure you ran cd dep-graph-solver before the touch command. If ls shows nothing, run pwd to check your current directory. You should be inside ~/Desktop/dep-graph-solver.

Help me figure out why my project files are missing.

Create the sample input file

You need test data to run your solver against. This file defines a simple set of tasks and their dependencies using a format where each line says "task: prerequisite1 prerequisite2".

  • Create tasks.txt with sample dependency data by running this command:
cat > tasks.txt << 'EOF'
deploy: build test
build: compile
test: compile
compile:
docs:
EOF

What does this input mean?

Each line defines a task and what it depends on. deploy: build test means "deploy" cannot run until both "build" and "test" finish.

A task with nothing after the colon (like compile:) has no prerequisites. It can run immediately. These are the starting points your algorithm will find first.

  • Verify the file contents by running:
cat tasks.txt

You should see all five lines printed back, matching what you entered above.

File is empty or has extra characters?

If cat tasks.txt shows nothing, the heredoc command may have failed. Delete the file with rm tasks.txt and run the cat > tasks.txt << 'EOF' block again. Make sure you type EOF on its own line at the end.

Help me create tasks.txt with the correct content.

✔️ Awesome, I've got everything!

Great. Double check you have all four files by running ls one more time. You should see graph.py, main.py, tasks.txt, and visualize.py.

ⓧ I'd like to double check the full code

Your tasks.txt file should contain exactly this:

deploy: build test
build: compile
test: compile
compile:
docs:

The three Python files (graph.py, main.py, visualize.py) should exist but be empty. You'll add code to them in the next steps.

Finally, open this project folder in VS Code so the file explorer shows your files.

  • Click File in the VS Code menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the dep-graph-solver folder, then click Open.

You should see all four files listed in the Explorer sidebar on the left.

Your project structure is ready and Python is confirmed. Next up, you'll build the graph data structure and get your first topological sort running.

Build the Graph and Get Your First Sort

Your project folder is ready with empty Python files and a sample task file. Now you need a way to represent tasks and their dependencies as a data structure, and an algorithm that can read that structure and produce a valid execution order.

In this step, you will build a directed graph class that tracks which tasks depend on which, then implement Kahn's algorithm to sort those tasks into an order where every prerequisite finishes before the task that needs it. By the end, you will run your solver on the sample tasks file and see a valid build order printed to the terminal.

In this step, get ready to:
  • Implement a graph data structure that tracks tasks and their dependencies.
  • Implement Kahn's algorithm for topological sorting.
  • Wire up a CLI entry point that parses a task file and prints the build order.
Implement the graph data structure

A dependency graph needs two pieces of information for each task: which other tasks depend on it (its dependents), and which tasks it depends on (its prerequisites). You will store both using dictionaries inside a DependencyGraph class.

  • Open graph.py in VS Code and add the class definition with its core add_dependency method:
import sys
from collections import deque


class DependencyGraph:
    def __init__(self):
        self.adj = {}       # node -> list of dependents (nodes that depend on it)
        self.prereqs = {}   # node -> set of prerequisites

    def add_dependency(self, task, *prerequisites):
        if task not in self.adj:
            self.adj[task] = []
        if task not in self.prereqs:
            self.prereqs[task] = set()
        for prereq in prerequisites:
            if prereq not in self.adj:
                self.adj[prereq] = []
            if prereq not in self.prereqs:
                self.prereqs[prereq] = set()
            self.adj[prereq].append(task)
            self.prereqs[task].add(prereq)

What does this code do?

This class stores a graph using two dictionaries:

  • self.adj maps each task to a list of tasks that depend on it. When compile finishes, you can look up which tasks are now unblocked.
  • self.prereqs maps each task to the set of tasks it requires. This tells you how many prerequisites a task is still waiting on.
  • add_dependency registers both directions: it adds the task as a dependent of each prerequisite, and records each prerequisite in the task's prereqs set.

Next, you need a way to read task definitions from a file. The format is simple: each line has a task name, a colon, then space-separated prerequisites.

  • Add the parse_file classmethod and two helper methods below the add_dependency method in graph.py:
    @classmethod
    def parse_file(cls, filepath):
        graph = cls()
        with open(filepath, "r") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                parts = line.split(":")
                task = parts[0].strip()
                prerequisites = parts[1].strip().split() if len(parts) > 1 and parts[1].strip() else []
                graph.add_dependency(task, *prerequisites)
        return graph

    def get_all_nodes(self):
        return list(self.adj.keys())

    def get_edges(self):
        edges = []
        for node, dependents in self.adj.items():
            for dep in dependents:
                edges.append((node, dep))
        return edges

What does this code do?

  • parse_file reads each line of the input file, splits on the colon to separate the task name from its prerequisites, and calls add_dependency for each entry. Lines that are blank or start with # are skipped.
  • get_all_nodes and get_edges are utility methods you will use in a later step to generate visual diagrams of the graph.
  • Save graph.py.
  • Confirm the parser works by running this in your terminal:
python3 -c "from graph import DependencyGraph; g = DependencyGraph.parse_file('tasks.txt'); print(g.get_all_nodes())"

You should see a list of all five task names printed, such as ['deploy', 'build', 'test', 'compile', 'docs'].

Seeing a ModuleNotFoundError?

  • Make sure your terminal is inside the dep-graph-solver folder. Run ls and confirm you can see graph.py in the listing.
  • Check that you saved graph.py after pasting the code. Unsaved changes are invisible to Python.

Still stuck? Help me debug this ModuleNotFoundError for graph.py

Implement Kahn's algorithm for topological sort

Kahn's algorithm finds a valid execution order by repeatedly picking tasks that have zero unmet prerequisites. It starts by collecting every task with no prerequisites, emits them, then decrements the in-degree of each task that depended on the emitted ones.

  • Add the topological_sort method to your DependencyGraph class, below the get_edges method in graph.py:
    def topological_sort(self):
        in_degree = {node: len(self.prereqs[node]) for node in self.adj}
        ready = deque([node for node, deg in in_degree.items() if deg == 0])
        order = []

        while ready:
            node = ready.popleft()
            order.append(node)
            for dependent in self.adj[node]:
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    ready.append(dependent)

        if len(order) != len(self.adj):
            return order, False  # incomplete sort, cycle exists
        return order, True

How does Kahn's algorithm work?

  • First, it calculates the in-degree of every node. The in-degree is the number of prerequisites a task is waiting on.
  • It collects all nodes with in-degree zero into a ready queue. These are tasks with no unmet dependencies.
  • Each iteration pops a ready node, adds it to the output, and decrements the in-degree of every task that depends on it. If any dependent's in-degree drops to zero, it enters the ready queue.
  • The final check compares the output length to the total node count. If they differ, some nodes were never emitted, which means a cycle exists. The method returns a tuple of the order and a boolean indicating success.
  • Save graph.py.
Wire up main.py and print the build order

The graph and algorithm are ready. Now you need a CLI entry point that accepts a filename, parses it into a graph, runs the sort, and prints the result.

  • Open main.py in VS Code and add the following code:
import sys
from graph import DependencyGraph


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 main.py <tasks_file> [--visualize] [--parallel] [--compare]")
        sys.exit(1)

    filepath = sys.argv[1]
    flags = sys.argv[2:]

    graph = DependencyGraph.parse_file(filepath)

    order, success = graph.topological_sort()

    print("Build order:")
    for task in order:
        print(f"  {task}")


if __name__ == "__main__":
    main()

What does this code do?

  • The argument check ensures the learner provides a task file when running the script. Without one, it prints usage instructions and exits.
  • flags = sys.argv[2:] captures any extra command-line flags. You will use these in later steps to trigger visualization and parallel sorting.
  • The script parses the file into a graph, runs topological sort, and prints each task in the computed order.
  • Save main.py.

Before you run the solver, think about what you expect to see. Given that deploy depends on build and test, and both of those depend on compile, which task do you think will appear first in the output?

  • Run your solver on the sample tasks file:
python3 main.py tasks.txt

You should see output like this:

Build order:
  compile
  docs
  build
  test
  deploy

compile appears first because it has no prerequisites. deploy appears last because everything else must finish before it can run.

Why might the order differ slightly?

Topological sort does not produce one unique answer. Any order where every prerequisite comes before its dependent is valid. Your output might swap docs and compile (both have zero prerequisites), or swap build and test (both only need compile). As long as compile appears before build and test, and deploy appears last, the output is correct.

Seeing an empty output or an error?

  • If you see FileNotFoundError, confirm that tasks.txt exists in the same folder as main.py. Run ls to check.
  • If you see ImportError, make sure graph.py is saved and the class name is exactly DependencyGraph (capital D, capital G).
  • If you see Build order: with nothing listed after it, check that tasks.txt is not empty and each line uses a colon to separate the task from its prerequisites.

Help me debug why my topological sort is not printing any tasks

✔️ Awesome, I've got everything!

Great. Double check you have saved both graph.py and main.py.

ⓧ I'd like to double check the full code

Here is the full graph.py at this point:

import sys
from collections import deque


class DependencyGraph:
    def __init__(self):
        self.adj = {}       # node -> list of dependents (nodes that depend on it)
        self.prereqs = {}   # node -> set of prerequisites

    def add_dependency(self, task, *prerequisites):
        if task not in self.adj:
            self.adj[task] = []
        if task not in self.prereqs:
            self.prereqs[task] = set()
        for prereq in prerequisites:
            if prereq not in self.adj:
                self.adj[prereq] = []
            if prereq not in self.prereqs:
                self.prereqs[prereq] = set()
            self.adj[prereq].append(task)
            self.prereqs[task].add(prereq)

Continuing in graph.py, the class methods:

    @classmethod
    def parse_file(cls, filepath):
        graph = cls()
        with open(filepath, "r") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                parts = line.split(":")
                task = parts[0].strip()
                prerequisites = parts[1].strip().split() if len(parts) > 1 and parts[1].strip() else []
                graph.add_dependency(task, *prerequisites)
        return graph

    def get_all_nodes(self):
        return list(self.adj.keys())

    def get_edges(self):
        edges = []
        for node, dependents in self.adj.items():
            for dep in dependents:
                edges.append((node, dep))
        return edges

And the topological sort method (still inside the class):

    def topological_sort(self):
        in_degree = {node: len(self.prereqs[node]) for node in self.adj}
        ready = deque([node for node, deg in in_degree.items() if deg == 0])
        order = []

        while ready:
            node = ready.popleft()
            order.append(node)
            for dependent in self.adj[node]:
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    ready.append(dependent)

        if len(order) != len(self.adj):
            return order, False  # incomplete sort, cycle exists
        return order, True

Here is the full main.py:

import sys
from graph import DependencyGraph


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 main.py <tasks_file> [--visualize] [--parallel] [--compare]")
        sys.exit(1)

    filepath = sys.argv[1]
    flags = sys.argv[2:]

    graph = DependencyGraph.parse_file(filepath)

    order, success = graph.topological_sort()

    print("Build order:")
    for task in order:
        print(f"  {task}")


if __name__ == "__main__":
    main()

Your solver correctly reads a task file and produces a valid build order. Next up, you will feed it a graph with a circular dependency and discover a subtle bug: the algorithm silently drops tasks instead of warning you.

Feed It a Cycle

Your solver handles a clean dependency graph perfectly. But real-world task graphs are not always clean.

What happens when someone defines a circular dependency, where Task A needs Task B, Task B needs Task C, and Task C needs Task A? The current algorithm has a hidden limitation: it silently produces an incomplete result. In this step, you will expose that limitation by feeding the solver a broken graph.

In this step, get ready to:
  • Create a test file with a circular dependency.
  • Observe how Kahn's algorithm silently drops tasks caught in a cycle.
  • Add a warning that tells the user something went wrong.
Create a cycle input file

A circular dependency means Task A requires Task B, which requires Task C, which requires Task A again. No task can ever start because each one is waiting for another.

  • Create a new file called cycle_tasks.txt in your dep-graph-solver folder with the following content:
auth: cache
db: auth
cache: db
frontend: auth

What does this define?

Three tasks form a cycle: auth needs cache, cache needs db, and db needs auth. None of these three can ever start.

The fourth task, frontend, depends on auth. Since auth is trapped in the cycle, frontend can never start either.

  • Save cycle_tasks.txt.
  • Confirm the file exists by running:
ls

You should see cycle_tasks.txt alongside tasks.txt, graph.py, main.py, and visualize.py in the output.

Run the solver on the broken graph

Before you run this, think about what you expect to happen. You defined 4 tasks, and all of them have at least one prerequisite that cannot be satisfied. What do you think Kahn's algorithm will do?

  • Run your solver on the cycle file:
python3 main.py cycle_tasks.txt

You should see this output:

Build order:

That is it. Just the header, with zero tasks listed underneath. All four tasks silently vanished from the output with no error, no warning, nothing.

Why did this happen?

Kahn's algorithm starts with nodes that have zero prerequisites (in-degree of 0). In this graph, every single node has at least one prerequisite. The ready queue starts empty, so the while loop never executes.

The algorithm simply stops. It does not crash or raise an error. It returns whatever partial order it managed to build (in this case, nothing). Your main.py then prints that empty list without questioning it.

This is the dangerous kind of bug. The program does not crash. It just gives you an incomplete answer and moves on. If this were a real build system, tasks would be silently skipped.

Add a warning when the sort is incomplete

Your topological_sort() method already detects this problem. It compares the number of sorted nodes to the total node count and returns False when they do not match. The problem is that main.py ignores that signal.

  • In main.py, find this line:
order, success = graph.topological_sort()

    print("Build order:")
    for task in order:
        print(f"  {task}")
  • Replace the print("Build order:") section with this code that checks the success boolean first:
order, success = graph.topological_sort()

    if not success:
        print("Warning: not all tasks could be sorted (possible cycle)")
        return

    print("Build order:")
    for task in order:
        print(f"  {task}")

What does this code do?

The if not success: check acts on the boolean that topological_sort() already returns. When the sort is incomplete, the function prints a clear warning and exits early instead of silently printing an empty or partial result.

  • Save main.py.
  • Run the solver on the cycle file again:
python3 main.py cycle_tasks.txt

You should now see:

Warning: not all tasks could be sorted (possible cycle)
  • Confirm the valid input still works by running:
python3 main.py tasks.txt

You should see the full build order printed as before, with all five tasks listed.

Not seeing the warning?

Make sure you saved main.py after editing. Unsaved changes will not take effect.

Check that the if not success: block is placed BEFORE the print("Build order:") line, not after it.

Help me debug my cycle detection warning.

✔️ Awesome, I've got everything!

Great. Double check you have saved both cycle_tasks.txt and main.py.

ⓧ I'd like to double check the full code

Here is the full main.py at this point:

import sys
from graph import DependencyGraph


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 main.py <tasks_file> [--visualize] [--parallel] [--compare]")
        sys.exit(1)

    filepath = sys.argv[1]
    flags = sys.argv[2:]

    graph = DependencyGraph.parse_file(filepath)

    order, success = graph.topological_sort()

    if not success:
        print("Warning: not all tasks could be sorted (possible cycle)")
        return

    print("Build order:")
    for task in order:
        print(f"  {task}")


if __name__ == "__main__":
    main()

The graph.py file is unchanged from the previous step.

The gap that remains

You now know THAT a cycle exists. But the warning does not tell you WHERE the cycle is. If this were a build system with 50 tasks, you would have to manually hunt through your dependency definitions to find the loop.

The next step adds a DFS-based cycle finder that pinpoints the exact tasks forming the loop.

Your solver now catches incomplete sorts and raises a flag. Next up, you will teach it to trace the exact cycle path so the developer knows precisely which dependencies to fix.

Pinpoint the Cycle with DFS

Your solver now warns that a cycle exists. But that warning is like being told "there's a bug in your code" without being told which line.

A developer staring at a config with 50 tasks needs to know exactly which tasks form the loop so they can break it. In this step, you'll implement a DFS-based cycle finder that traces the precise chain of nodes caught in the loop.

In this step, get ready to:
  • Implement a find_cycle() method using DFS three-color marking.
  • Update main.py to print the exact cycle path.
  • Confirm both valid and cyclic inputs produce correct output.
Implement the cycle finder with DFS

Kahn's algorithm can detect that a cycle exists (the output is incomplete), but it cannot locate where. A DFS traversal with three-color marking solves this.

What is three-color marking?

Each node starts as WHITE (unvisited). When DFS enters a node, it turns GRAY (currently on the recursion stack). When DFS finishes with a node and backtracks, it turns BLACK (fully explored).

If DFS ever reaches a GRAY node, that means it has found a node that is already on the current path. That back-edge proves a cycle exists, and the stack tells you exactly which nodes form it.

  • In graph.py, find the topological_sort() method. Add the following find_cycle() method directly below it (before get_all_nodes()):
    def find_cycle(self):
        WHITE, GRAY, BLACK = 0, 1, 2
        color = {node: WHITE for node in self.adj}
        stack = []

        def dfs(u):
            color[u] = GRAY
            stack.append(u)
            for v in self.adj[u]:
                if color[v] == GRAY:
                    cycle_start = stack.index(v)
                    return stack[cycle_start:] + [v]
                if color[v] == WHITE:
                    result = dfs(v)
                    if result:
                        return result
            stack.pop()
            color[u] = BLACK
            return None

        for node in self.adj:
            if color[node] == WHITE:
                cycle = dfs(node)
                if cycle:
                    return cycle
        return None

What does this code do?

  • WHITE, GRAY, BLACK = 0, 1, 2 defines the three states as simple integers.
  • The stack list tracks the current DFS path. When DFS visits a GRAY node, it slices the stack from that node's position to the end to extract the exact cycle.
  • Appending v again at the end (stack[cycle_start:] + [v]) makes the cycle visually loop back: "auth -> cache -> db -> auth".
  • The outer loop ensures every connected component is checked, even if the graph is disconnected.
  • Save graph.py.
Update main.py to call find_cycle()

The cycle finder is ready, but main.py still prints the generic warning. You need to wire it up so the solver calls find_cycle() when the sort comes back incomplete.

  • In main.py, find this block:
    if not success:
        print("Warning: not all tasks could be sorted (possible cycle)")
        return
  • Replace the entire if-block with this updated version:
    if not success:
        # Call find_cycle() to locate the exact loop
        cycle = graph.find_cycle()
        if cycle:
            print("Cycle detected: " + " -> ".join(cycle))
        else:
            print("Warning: not all tasks could be sorted (possible cycle)")
        return

What changed?

Instead of immediately printing the generic warning, the code now calls graph.find_cycle(). If it returns a cycle, it prints the exact chain of tasks forming the loop. The generic warning remains as a fallback for edge cases where the sort is incomplete but no cycle is found by DFS.

  • Save main.py.
Test both input files

You need to confirm two things: the valid graph still sorts correctly, and the cyclic graph now names the exact loop.

  • First, verify the happy path still works. Run the valid task file:
python3 main.py tasks.txt

You should see the build order printed just like before (e.g., compile, docs, build, test, deploy). No cycle, no warnings.

Now for the real test. Before you run the cycle file, predict: what nodes do you think form the loop in cycle_tasks.txt?

  • Run the cyclic task file:
python3 main.py cycle_tasks.txt

You should see output naming the exact cycle, like:

Cycle detected: auth -> cache -> db -> auth

The cycle is spelled out as a chain that loops back to its starting node. A developer can now look at that output and immediately see which dependency to break.

Not seeing the cycle output?

  • Make sure you saved both graph.py and main.py before running.
  • Check that find_cycle() is indented at the same level as topological_sort() (one level inside the class body).
  • Verify your updated if-block in main.py calls graph.find_cycle(), not just find_cycle() (it's a method on the graph object).

Help me debug my find_cycle() DFS implementation.

Why DFS instead of modifying Kahn's?

Kahn's algorithm processes nodes by removing them from the graph. Once it stops (because every remaining node has a non-zero in-degree), the "stuck" nodes are all mixed together. You know they're involved in cycles, but you cannot tell which specific nodes form a single loop.

DFS walks a path through the graph and remembers the exact trail. When it loops back to a node already on that trail, the trail IS the cycle. Different algorithms for different questions.

✔️ Awesome, I've got everything!

Great. Make sure both graph.py and main.py are saved.

ⓧ I'd like to double check the full code

Both files are over 30 lines at this point. Scroll up and confirm the following are present in order.

In graph.py:

  • Imports (sys, deque from collections).
  • DependencyGraph class with __init__, add_dependency, parse_file, topological_sort, find_cycle, get_all_nodes, and get_edges methods.
  • find_cycle() sits between topological_sort() and get_all_nodes().

In main.py, confirm the full file matches this:

import sys
from graph import DependencyGraph


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 main.py <tasks_file> [--visualize] [--parallel] [--compare]")
        sys.exit(1)

    filepath = sys.argv[1]
    flags = sys.argv[2:]

    graph = DependencyGraph.parse_file(filepath)

    order, success = graph.topological_sort()

    if not success:
        cycle = graph.find_cycle()
        if cycle:
            print("Cycle detected: " + " -> ".join(cycle))
        else:
            print("Warning: not all tasks could be sorted (possible cycle)")
        return

    print("Build order:")
    for task in order:
        print(f"  {task}")


if __name__ == "__main__":
    main()

Your solver now handles both cases like a real build tool: valid graphs produce execution order, and broken graphs tell you exactly what to fix. Next up, you'll make the graph visible by generating a diagram you can actually look at.

Visualize the Graph with Mermaid

Your solver now catches cycles and names the exact tasks forming the loop. But reading a flat list of task names still forces you to mentally reconstruct the graph.

A visual diagram makes the dependency structure immediately obvious. Mermaid is the industry-standard text-to-diagram format, and VS Code renders it natively in its built-in Markdown preview. In this step, you will generate a Mermaid flowchart from your graph and view it as a rendered diagram without installing anything extra.

In this step, get ready to:
  • Implement a Mermaid diagram generator that converts your graph into a flowchart string.
  • Wire up a --visualize flag that writes the diagram to a Markdown file.
  • View rendered dependency diagrams with cycle highlighting in VS Code.
Implement the Mermaid generator

A Mermaid flowchart is plain text that follows a specific syntax. You declare a direction (flowchart TD for top-down), list nodes, and draw edges with --> arrows.

  • Open visualize.py in VS Code and add the following function:
def generate_mermaid(graph, cycle_nodes=None):
    # Start the Mermaid fenced code block and declare top-down direction
    lines = ["```mermaid", "flowchart TD"]

    # Declare each node with a label
    for node in sorted(graph.get_all_nodes()):
        lines.append(f"    {node}[{node}]")

    # Draw directed edges from prerequisites to dependents
    for source, target in graph.get_edges():
        lines.append(f"    {source} --> {target}")

    # If cycle nodes are provided, highlight them in red
    if cycle_nodes:
        lines.append("    classDef cycle fill:#ff6b6b,stroke:#c0392b;")
        node_list = ",".join(cycle_nodes)
        lines.append(f"    class {node_list} cycle")

    lines.append("```")
    return "\n".join(lines)

What does this code do?

The function builds a list of strings that form a valid Mermaid diagram. Each node gets an explicit declaration (node[node]) so Mermaid knows what to render.

The cycle_nodes parameter is optional. When provided, it uses Mermaid's classDef syntax to paint those nodes red, making circular dependencies visually obvious.

  • Save visualize.py.

Seeing a syntax error in the code?

The backtick characters inside the string ["```mermaid", ...] are three literal backticks. Make sure you have exactly three, not one or two.

help me fix a syntax error in my generate_mermaid function

Wire up the --visualize flag in main.py

The generator function exists, but nothing calls it yet. You need to import it and call it when the learner passes --visualize on the command line.

  • In main.py, find this import line at the top of the file:
from graph import DependencyGraph
  • Add this new import directly below it:
from visualize import generate_mermaid

Now you need to add the visualization logic for the success path. When the sort completes and --visualize is present, the program should write the diagram to graph.md.

  • In main.py, scroll down to the end of the main() function and find these lines:
    print("Build order:")
    for task in order:
        print(f"  {task}")
  • Add the following block directly below that for loop:
    # Write Mermaid diagram to file if --visualize flag is present
    if "--visualize" in flags:
        mermaid = generate_mermaid(graph)
        with open("graph.md", "w") as f:
            f.write("# Dependency Graph\n\n")
            f.write(mermaid)
        print("\nDiagram written to graph.md")

What does this block do?

When the user passes --visualize, the code generates the Mermaid string and writes it to graph.md with a Markdown heading. The file is a complete Markdown document that VS Code can render directly.

  • Save main.py and test the valid graph by running:
python3 main.py tasks.txt --visualize

You should see the build order printed, followed by Diagram written to graph.md.

Seeing an ImportError?

Make sure you saved visualize.py before running. Python cannot import a function from an empty file.

Also check that visualize.py is in the same folder as main.py (your dep-graph-solver directory).

help me fix an ImportError for generate_mermaid

Now add visualization support for the cycle detection path too. When a cycle is detected and --visualize is present, you want the cycle nodes highlighted in red.

  • In main.py, find the if not success: block. Locate this line at the end of that block:
        return
  • Add the following code directly above that return statement (at the same indentation level, inside if not success:):
        # Generate diagram with cycle nodes highlighted in red
        if "--visualize" in flags:
            cycle_nodes = set(cycle[:-1]) if cycle else set()
            mermaid = generate_mermaid(graph, cycle_nodes=cycle_nodes)
            with open("graph.md", "w") as f:
                f.write("# Dependency Graph (Cycle Detected)\n\n")
                f.write(mermaid)
            print("Diagram written to graph.md")

Why cycle[:-1]?

The find_cycle() method returns the cycle with the starting node repeated at the end (e.g., ["auth", "cache", "db", "auth"]). Slicing with [:-1] removes the duplicate so each node is highlighted exactly once in the diagram.

  • Save main.py.
  • Test the cycle graph by running:
python3 main.py cycle_tasks.txt --visualize

You should see the cycle detection message followed by Diagram written to graph.md.

✔️ Awesome, I've got everything!

Great. Make sure both visualize.py and main.py are saved before moving on to view the diagrams.

ⓧ I'd like to double check the full code

Your visualize.py should contain:

def generate_mermaid(graph, cycle_nodes=None):
    lines = ["```mermaid", "flowchart TD"]

    for node in sorted(graph.get_all_nodes()):
        lines.append(f"    {node}[{node}]")

    for source, target in graph.get_edges():
        lines.append(f"    {source} --> {target}")

    if cycle_nodes:
        lines.append("    classDef cycle fill:#ff6b6b,stroke:#c0392b;")
        node_list = ",".join(cycle_nodes)
        lines.append(f"    class {node_list} cycle")

    lines.append("```")
    return "\n".join(lines)

For main.py, confirm you have these pieces in order:

  • The from visualize import generate_mermaid import at the top.
  • The if "--visualize" in flags: block inside the if not success: branch, just above return.
  • The if "--visualize" in flags: block after the Build order: print loop at the end of the function.
View the rendered diagrams in VS Code

The graph.md file contains raw Mermaid syntax wrapped in a fenced code block. VS Code 1.121+ renders Mermaid diagrams natively in its Markdown preview.

  • First, regenerate the valid graph diagram by running:
python3 main.py tasks.txt --visualize
  • Open graph.md in VS Code by clicking on it in the Explorer sidebar.
  • Press Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows) to open the Markdown preview.

Before you look, what do you expect to see? Think about which tasks have arrows pointing to which.

You should see a top-down flowchart with compile at the top, arrows flowing down to build and test, then both pointing to deploy. The docs node sits alone with no connections.

  • Now regenerate with the cycle graph by running:
python3 main.py cycle_tasks.txt --visualize
  • Switch back to the graph.md tab in VS Code. The preview updates automatically when the file changes.

You should see the same flowchart structure, but now the auth, cache, and db nodes are highlighted in red. The circular dependency is immediately visible.

Seeing raw text instead of a diagram?

If the Mermaid code block renders as plain text, your VS Code version may be older than 1.121. Update VS Code to the latest version, or install the Markdown Preview Mermaid Support extension (bierner.markdown-mermaid) from the Extensions panel.

Also check that graph.md starts with the three-backtick mermaid fence. Open the raw file and confirm the first content line is ```mermaid.

help me get Mermaid diagrams rendering in VS Code Markdown preview

Why Mermaid over other diagram tools?

Tools like Graphviz produce beautiful diagrams but require installing system packages. Mermaid diagrams are plain text that renders anywhere Markdown is supported. GitHub, GitLab, Notion, and VS Code all render Mermaid natively.

Your dependency solver now has a complete visual output. Next up in the Secret Mission, you will group independent tasks into parallel execution layers and validate your implementation against Python's built-in graphlib.

Secret mission

Add Parallel Execution Layers

Real build systems don't just need order. They need to know which tasks can run at the same time. Can you group independent tasks into parallel layers and prove your implementation matches Python's built-in graphlib?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with no cloud resources or paid APIs, so there are no ongoing costs.

Decide whether to keep your project files for future reference or delete them entirely.

Resources you used:

  • The dep-graph-solver project folder on your Desktop (contains graph.py, main.py, visualize.py, tasks.txt, cycle_tasks.txt, and generated graph.md).

Keep everything running

No action needed. Choose this if you want to keep your dependency graph solver as a reference tool or plan to extend it further.

  • Your solver is a useful utility for reasoning about task dependencies in any future project.
  • You could extend it to read from package.json or Makefile formats, add duration estimates, or implement critical path analysis.

Pause - I'll come back to this later

There are no running processes or background services to stop. Your project is just files on disk, so it costs nothing to leave in place.

  • Close the VS Code workspace if you want to free up the editor for other projects.
  • Your files will be right where you left them when you come back.

Delete - I don't want to use this again

Remove the entire project folder to start fresh.

  • Close the dep-graph-solver workspace in VS Code.
  • Delete the project folder by running this command in your terminal:

macOS

cd ~/Desktop
rm -rf dep-graph-solver

Windows

cd ~/Desktop
Remove-Item -Recurse -Force dep-graph-solver
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see dep-graph-solver in the output.

Nice Work!

Nice Work!

You just built a fully functional task dependency solver from scratch. It parses dependencies, computes valid execution orders, catches circular references, and renders the whole thing as a visual diagram.

Here's what you learned:

  • Implemented Kahn's algorithm for topological sorting from scratch, using in-degree counting to emit tasks in a valid build order.
  • Added DFS-based cycle detection with three-color marking that pinpoints the exact loop path, not just "a cycle exists".
  • Generated Mermaid flowchart diagrams that render natively in VS Code's built-in Markdown preview, with color-coded nodes highlighting cycles or parallel layers.
  • Secret Mission: Grouped tasks into parallel execution layers and validated your implementation against Python's built-in graphlib.TopologicalSorter.

Ready to quiz yourself?