I've participated in Advent of Code for the last few years, and almost every year, there's a scheduling challenge that requires topologically sorting some graph. To solve these problems, I always implement my own topological sort from scratch:

from copy import deepcopy

def topological_sort(graph):
    nodes = set(graph.keys()).union(*graph.values())
    edges = deepcopy(graph)

    ordering = []
    while edges:
        next_nodes = nodes - edges.keys()
        for node in next_nodes:
            nodes.remove(node)
            ordering.append(node)

            children_to_delete = set()
            for child, parents in edges.items():
                if node in parents:
                    parents.remove(node)
                if not parents:
                    children_to_delete.add(child)
            for child in children_to_delete:
                del edges[child]

    ordering.extend(list(nodes))
    return ordering
    
graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
topological_sort(graph)  # ['A', 'C', 'B', 'D']

But this year, I might not do that! The release of Python 3.9 came with a new standard library called graphlib that will be fleshed out over time to provide various graph functionalities. As of today, it contains only one main tool, and you guessed it, it's the topological sort!

To use it, the input graph should be a dictionary that maps each child node to the set of parents that it depends on. From there, there are two primary ways to retrieve the topological ordering. The first uses static_order() to return an iterator:

import graphlib

graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
sorter = graphlib.TopologicalSorter(graph)

print(list(sorter.static_order())  # ('A', 'C', 'B', 'D')

The second allows more flexibility for intermediate processing steps if you need it. It uses the prepare(), is_active(), get_ready(), and done() methods:

import graphlib

graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
sorter = graphlib.TopologicalSorter(graph)

ordering = []
sorter.prepare()
while sorter.is_active():
    next_nodes = sorter.get_ready()
    for node in next_nodes:
        ordering.append(node)
        # add intermediate processing here!
        sorter.done(node)

print(ordering)  # ['A', 'C', 'B', 'D']