LESSON 54 · DEEPER PYTHON30 min

Schedule Jobs with heapq

Use heapq and an insertion counter to process lower numeric priorities first while preserving ties.

Before you start: Lists · Tuples and Sets · Loops

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Schedule Jobs with heapq

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A support queue serves the oldest job first, but a priority queue has a different promise: serve the most urgent priority. In this lesson a smaller integer means more urgent. If test and deploy both have priority one, we also want their original arrival order to decide the tie. That gives us a clear expected sequence: test, deploy, then build at priority two. Python's heapq can efficiently push new jobs and pop the next minimum. Yet a simple tuple of priority and payload can fail when priorities tie. We will reproduce a real TypeError using dictionary payloads, inspect the tuple comparison that causes it, and add a counter that makes ties stable.

  2. 00:43

    Then we will build schedule_jobs as a small, checked function. A heap is not a fully sorted list; its useful guarantee is about the smallest entry at the root. The companion lesson offers runnable examples and a task checker, so you can observe both correct ordering and wrong assumptions. Before running the broken code, predict why the second push might need to look beyond the equal priority numbers. Python has to compare something to decide which heap entry comes first. The first push succeeds because the heap has nothing else to compare. On the second push, the priority values are both one. Python compares tuple fields from left to right.

  3. 01:25

    Since the first fields tie, it tries to order the second fields: two dictionaries. Dictionaries do not define a less-than ordering, so the comparison raises TypeError. Catching it here lets our demonstration continue, but catching and ignoring this error would not make a working queue. Even if the payloads were strings and the comparison succeeded, their alphabetic order might replace the arrival-order policy we wanted. The failure is therefore both a type problem and a contract problem. We need a second field that is always comparable and represents an intentional rule. An increasing insertion index does exactly that.

  4. 02:05

    With entries shaped as priority, index, payload, equal priorities are resolved by index. The payload is never reached as a comparison field because two arrivals do not share the same index. This also lets payloads be dictionaries, callables, or other objects that should not decide scheduling order. Before the fix, inspect a simple tuple comparison to see the rule directly. When first fields tie at one, zero is less than one, so the earlier index sorts first. A priority of two is not less than priority one, regardless of the second field. The minimum tuple here is one, three. This is the ordering heapq uses when it compares tuple entries.

  5. 02:48

    It does not know what priority means in our application; it simply relies on Python's normal ordering. By placing priority first and arrival index second, we encode the scheduling policy. The third field can be the actual task name or payload. We do not need to manually sort the whole heap each time a new job arrives. Push and pop maintain the heap invariant in logarithmic time. Printing the internal heap list, however, would not prove global ordering; elements below the root may appear in an arrangement that is valid as a heap but not sorted from left to right. To get the service sequence, repeatedly call heappop.

  6. 03:28

    For a one-time small collection, sorted can be simpler, but a heap teaches a useful pattern for work that arrives dynamically. We will now apply the tuple key and verify the queue does not mutate its input list. The two priority-one jobs leave first. test precedes deploy because test had the smaller arrival index, not because its name happens to sort earlier. build leaves last at priority two. The second line shows that the caller's jobs list is unchanged; the function built its own heap rather than modifying the input. enumerate supplies a unique index for each job. A lower numeric priority wins first, and only equal priorities consult that index.

  7. 04:13

    The heappop loop extracts the task name from field two, counting from zero, and returns a simple list of names. This implementation accepts integer priorities, including negative integers. If your application uses labels such as urgent and normal, map them to a consistent comparable key before pushing. Mixing a string priority with an integer priority can raise a different TypeError, and this counter does not repair invalid first fields. The counter also does not make the raw heap list sorted. It makes repeated pops deterministic. Read the tuple shape aloud before using it: priority, arrival index, task.

  8. 04:53

    If a scheduling bug appears, inspect those fields in that order and compare them with the stated policy. The checks confirm the main order, a negative priority, stable ties, empty input, and dictionary payloads that used to trigger TypeError. They also confirm that submitting the payload list does not mutate it. The function was described with job names, but its ordering logic works with those unorderable dictionaries because the index resolves each tie before the payload is considered. An empty heap produces an empty result without a special exception. Keep the difference between priority and arrival in mind: priority dominates; arrival matters only when priorities tie.

  9. 05:37

    The browser lesson asks you to implement the same core function. If your first attempt fails, print each heap entry as it is pushed and say which tuple field should settle a tie. Do not fix the problem by converting a dictionary to a string solely to make comparison possible; that invents a meaningless ordering. Add a deliberate comparable key. In larger systems you may also need cancellation, updated priorities, and concurrent producers. This small example is about the foundation: use heappush and heappop, encode policy in an ordered tuple, and prove the result with distinct jobs whose order you can recognize.

Understand the concept

A priority queue serves the item with the most urgent priority, not necessarily the oldest arrival. Python's heapq maintains a min-heap: heappop returns the smallest value according to Python's ordering. A heap is not a fully sorted list. Only its first item is guaranteed to be the smallest; do not display the raw heap and assume every position is sorted.

A common entry is (priority, task). Tuple comparison examines priority first and uses task as a tie breaker. That fails when two priorities tie and task values cannot be ordered, such as dictionaries. It can also give an unwanted alphabetical tie order. Add a monotonically increasing counter: (priority, arrival_index, task). Now equal priorities keep arrival order and Python never needs to compare task objects.

The task accepts a list of (priority, name) pairs. Lower integers are more urgent, and equal priorities must preserve input order. schedule_jobs returns the names in processing order, without changing the input list. The counter comes from enumerate, making the tie rule visible in the data structure rather than relying on a coincidental ordering of names.

For a one-time small list, sorted may be simpler. A heap becomes useful when jobs arrive over time and the program repeatedly inserts and removes the next priority. Its push and pop operations cost logarithmic time in the number of queued items. Priorities must remain comparable; mixing strings and integers as priority keys is a separate input-contract error.

  • heapq is a min-heap
  • Tuple ordering compares fields left to right
  • A counter makes equal-priority service stable
  • Push and pop are logarithmic operations

See it step by step

Read the code, predict the output, then compare it with the result.

01. A min-heap serves the smallest key first

PYTHON
import heapq
heap = []
for priority, name in [(3, 'cleanup'), (1, 'alert'), (2, 'report')]:
    heapq.heappush(heap, (priority, name))
while heap:
    print(heapq.heappop(heap)[1])
EXPECTED OUTPUT
alert
report
cleanup

Every pop chooses the smallest priority; the internal heap list need not be globally sorted.

02. Use a counter instead of comparing payloads

PYTHON
import heapq
heap = []
for index, (priority, payload) in enumerate([(1, {'id': 'A'}), (1, {'id': 'B'})]):
    heapq.heappush(heap, (priority, index, payload))
print(heapq.heappop(heap)[2]['id'])
print(heapq.heappop(heap)[2]['id'])
EXPECTED OUTPUT
A
B

The counter breaks equal-priority ties before Python reaches the unorderable dictionaries.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Use heap operations to choose the next priority

A min-heap places its smallest entry at the front. Push all three jobs, then repeatedly pop to observe their processing order. The integer priority is the first tuple field, so it controls which job leaves first. Do not infer the final order by simply printing the heap list; only repeated pops give a sorted sequence.

Priority one is more urgent than priority two under this contract. A different application might reverse the scale or use a richer key, but it should document that choice. A heap is especially useful when new work can arrive between removals, since it can push one new item without resorting the entire collection.

PYTHON
import heapq
heap = []
for priority, name in [(3, 'cleanup'), (1, 'alert'), (2, 'report')]:
    heapq.heappush(heap, (priority, name))
while heap:
    print(heapq.heappop(heap)[1])
EXPECTED OUTPUT
alert
report
cleanup
Follow the reasoning
  1. Each heappush maintains the heap invariant.
  2. heappop returns the smallest priority entry.
  3. The loop drains work in priority order.
02 / 03

Resolve ties before Python compares payloads

Tuple comparison moves to the next field when priorities tie. If that field is a dictionary, Python cannot order the two payloads and heappush may raise TypeError. Even string payloads might sort alphabetically instead of preserving arrival order. Add a numeric insertion index between priority and payload to define a stable tie rule.

The counter is unique for each arrival, so Python never needs to compare the dictionary payload. The first payload with priority one leaves first, then the second. The dictionaries themselves need no special comparison method. This pattern also works for tasks represented by objects, callables, or other values that should not decide queue order.

PYTHON
import heapq
heap = []
for index, (priority, payload) in enumerate([(1, {'id': 'A'}), (1, {'id': 'B'})]):
    heapq.heappush(heap, (priority, index, payload))
print(heapq.heappop(heap)[2]['id'])
print(heapq.heappop(heap)[2]['id'])
EXPECTED OUTPUT
A
B
Follow the reasoning
  1. Priority ties move comparison to the index.
  2. The earlier arrival has the smaller index.
  3. The payload is returned without being compared.
03 / 03

Return only the job names and preserve the input

The practice function accepts job pairs and builds a fresh heap. It adds the enumerate index as an internal tie breaker, then removes entries until the heap is empty. The caller receives just the names in processing order, not the internal priority tuples. This keeps the function contract small while retaining deterministic behavior.

Test empty work, negative priorities, and equal priorities. Negative numbers are valid integers in this exercise and leave before zero. The input list is only read, so a caller can submit it again or inspect the original arrival order afterward. A one-off sorted call could also solve this small exercise, but the heap makes the repeated push/pop mechanism visible.

PYTHON
import heapq

def schedule_jobs(jobs):
    heap = []
    for index, (priority, name) in enumerate(jobs):
        heapq.heappush(heap, (priority, index, name))
    ordered = []
    while heap:
        ordered.append(heapq.heappop(heap)[2])
    return ordered

jobs = [(2, 'build'), (1, 'test'), (1, 'deploy')]
print(schedule_jobs(jobs))
print(jobs)
EXPECTED OUTPUT
['test', 'deploy', 'build']
[(2, 'build'), (1, 'test'), (1, 'deploy')]
Follow the reasoning
  1. Build one tuple per job with priority and arrival index.
  2. Pop until every name is collected.
  3. The original jobs remain in arrival order.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnImplement schedule_jobs(jobs). Each job is (integer_priority, name). Return names from lowest numeric priority to highest, preserving original order when priorities tie. Do not mutate jobs. The prepared output is ['test', 'deploy', 'build'].
Need a hint?

Push (priority, index, name) for each item from enumerate(jobs). Pop while the heap is nonempty and collect the third field.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Which priority leaves a Python min-heap first?

    02.Why add an insertion index to (priority, index, task)?

    03.Is the raw heap list guaranteed to be globally sorted?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Unorderable payloads on a priority tie

    COMMON MISTAKE
    heapq.heappush(heap, (1, {'id': 'A'}))
    heapq.heappush(heap, (1, {'id': 'B'}))
    FIX
    heapq.heappush(heap, (1, 0, {'id': 'A'}))
    heapq.heappush(heap, (1, 1, {'id': 'B'}))

    What happens: The second push raises TypeError when dictionaries are compared.
    A unique counter is a comparable tie breaker.

    Treating the heap array as a sorted report

    COMMON MISTAKE
    print([task for _, task in heap])
    FIX
    while heap:
        print(heapq.heappop(heap))

    What happens: Items after the root may appear out of priority order.
    Repeated pops yield ordered priorities.

    Browse this lesson’s problem fixes

    Sources and further reading