TaskScheduler in python
Unused Loop Variables

for i in range(self.n):
    self.job_queue.put(None)


can be written like this

for _ in range(self.n):
    self.job_queue.put(None)


and any place where you don't use the loop variable. The _ lets you and other programmers know that that variable isn't used and should be ignored.

Naming

p should be process to be descriptive. While p in self.processes can be obvious, it's good to get in the practice of descriptive variable names.

Type Hints

This

def add_task(self, task_id, task, arg_list):


can instead be (assuming task is a str and arg_list is a list)

def add_task(self, task_id: int, task: str, arg_list: list) -&gt; None:


Using type hints allows you to see/show what types of parameters are accepted, and what type(s) is/are returned from the function.