Project: Dependency Task Scheduler
Project goal
Build a deterministic task order from in-memory Python objects. Only a task whose dependencies are all complete enters the ready min-heap; lower numeric priority runs first, and equal priority sorts by task_id. The project composes dict, set, an adjacency list, in-degree, heap, and output list, and validates the contract with 24 unit tests.
It neither reads JSON nor executes external tools, and it uses no threads, network, file writes, or secrets. Scheduling order is not proof of successful business execution. Persistence, retry, leases, authorization, and recovery for production workflows are outside this project.
Input contract
The task collection must be a built-in list or tuple. Generators and other one-shot iterators are intentionally rejected: after a scheduler consumes one, the caller cannot iterate it again, which makes “input unchanged” ambiguous. Each task is a mapping with exactly three fields:
{
"id": "validate",
"priority": 1,
"depends_on": ["extract"],
}| Field | Rule |
|---|---|
id | Built-in str; nonempty, no leading/trailing whitespace, globally unique |
priority | Required and type(value) is int; explicitly rejects bool |
depends_on | List; every item is a built-in str, nonempty, without leading/trailing whitespace; no duplicates |
Every dependency ID must exist. Direct self-dependency is rejected immediately; indirect cycles are reported after topological progress. Unknown/missing fields, invalid task objects, and dirty dependencies all become ScheduleInputError rather than leaking an AttributeError or a bare TypeError from an unhashable value.
Selection and invariants
by_id: id → immutable Task # single source of truth
remaining: id → unfinished predecessor count # in-degree working copy
dependents: predecessor id → successor id list # reverse adjacency list
ready: (priority, task_id) min-heap # current executable view
order: selected task_id values # resultThe following must hold:
remaininganddependentsrefer only to IDs inby_id.- Every ready task has in-degree zero.
- Each dependency edge decrements its successor’s in-degree once.
- A task enters the heap at most once and appears in output at most once.
- Priority is compared only among currently ready tasks.
- Scheduling does not mutate the caller’s list/tuple, task mapping, or dependency list.
- Repeated calls with identical input yield identical order.
Algorithm
parse_taskturns each untrusted mapping into a frozenTask._parse_tasksbuildsby_idand checks unique IDs and dependency existence.- Compute
remainingfor every task while buildingdependents. - Push every zero-in-degree task into the min-heap.
- Pop the current smallest
(priority, task_id)and decrement its successors. - Push a successor when its in-degree reaches zero, until
readyis empty. - If output count is below task count, one or more cycles block the remaining tasks.
Graph construction and in-degree update cost . Each task enters/leaves the heap at most once, adding . Total time is and auxiliary space . This is not a throughput claim for a real distributed scheduler.
Run it
From the vault root, run the demonstration:
python -B '.\docs-EN\data-structures\examples\task_scheduler.py'Expected:
execution order: extract -> validate -> summarize -> reviewRun tests:
python -B -m unittest discover `
-s '.\docs-EN\data-structures\examples' `
-p 'test_*.py' `
-v-B prevents .pyc generation. The example installs no dependency and writes no file. If python is not the expected interpreter, check Get-Command python, python --version, and sys.executable first.
Test matrix
| Category | Covered |
|---|---|
| Core | Collection root type; rejecting without consuming a one-shot iterator; empty input; one task; chain; diamond; independent components |
| Priority | Compare only within ready set; break peer ties by ID, not input order |
| Identity | Empty/trimmed/non-built-in string IDs, including unhashable subclasses; duplicate ID |
| Fields | Missing, unknown, non-mapping object, and normalized error for unhashable field names |
| Priority type | Reject bool, float, str, and None |
| Dependencies | Non-list, dirty member, duplicate, missing target, and direct self-dependency |
| Cycle | Indirect cycle plus downstream blocked by it; do not call every blocked node a cycle member |
| Properties | Input unchanged and repeated deterministic run |
Test count is evidence of the current implementation, not a goal to maximize. When changing the contract, update documentation and failure tests before implementation.
Required exercises
- Without reading the completed implementation, write
Taskand its parser from the data contract and make invalid-input tests fail first. - Draw predecessor map, successor map, and per-step in-degree change for a diamond dependency.
- Add two peer tasks, change tie policy to input FIFO, introduce
sequence, and update contract and tests. - Make
schedulereturn a ready snapshot per round without exposing the mutable internal heap. - Add a read-only
payloadextension; prove scheduling does not mutate it and state the limit of shallow copying.
Optional advanced work
- Reimplement a version that seeks only any valid topological order with
graphlib.TopologicalSorter, and compare its peer order. - Return stable error codes rather than depending on a complete English sentence, while retaining a human-readable message.
- Separately implement exact cycle-path or strongly-connected-component analysis, then distinguish cycle members from downstream blocked nodes; do not guess from the remaining set.
Do not add real API calls, a persistent queue, or asynchronous workers to this script. They blend data-structure correctness with side-effect recovery and make the result difficult to validate.
Common mistakes
- Treating
boolas a valid integer priority. - Starting scheduling before parsing every input, so a dirty priority is hidden by a cycle error.
- Rescanning every task and rebuilding dependencies every round, causing needless higher-order growth.
- Comparing incomparable payloads on equal priority.
- Treating heap removal as proof that an external task succeeded.
- Calling every remaining blocked node a cycle member.
- Using
assertfor untrusted input validation, which optimization mode may remove.
Project acceptance
- The demonstration and 24 tests pass on Python 3.11+.
- I can explain the responsibilities and invariants of the five internal structures.
- I can hand-calculate in-degree changes for a chain, diamond, and cyclic graph.
- I can prove that priority applies only inside
readyand peers sort by ID. - I can explain every term in .
- I can distinguish cycle members, nodes blocked by a cycle, and unrelated runnable components.
- I can name production semantics not covered instead of claiming external execution is verified.
After completion, proceed to Course Self-Check and Mastery Review.
References
Retrieved on 2026-07-14.