Skip to content

← True Parallelism and the Runtime step 9 of 18

Medium Framework

Typing Multiprocessing: Where the Annotations Lie to You

A --strict codebase that touches multiprocessing usually ends up with a scatter of # type: ignore at the most dangerous boundary in the program. Almost all of them come from one mistake, and knowing the right import removes them.

Why multiprocessing.Lock is not a type

def acquire(lock: multiprocessing.Lock) -> None: ...
error: Variable "multiprocessing.Lock" is not valid as a type  [valid-type]

At runtime, type(multiprocessing.Lock) is method. The names exported from the multiprocessing package are autogenerated bound methods of the default context object, not classes. The classes live in submodules:

You reached for The type actually is
multiprocessing.Lock / Event / Semaphore / Condition / Barrier multiprocessing.synchronize.*
multiprocessing.Queue / JoinableQueue / SimpleQueue multiprocessing.queues.*
multiprocessing.Value / Array multiprocessing.sharedctypes.*
multiprocessing.Pool multiprocessing.pool.Pool
Manager().dict() multiprocessing.managers.DictProxy
Pipe() ends multiprocessing.connection.Connection

The trap mypy does not catch

def enqueue(q: multiprocessing.Queue[int]) -> None: ...

This passes --strict. typeshed declares Queue generic, so the checker is satisfied. Then, at runtime:

>>> typing.get_type_hints(enqueue)
TypeError: 'method' object is not subscriptable

Because multiprocessing.Queue is a bound method, and methods are not subscriptable. Every library that reads annotations at runtime — pydantic, FastAPI, attrs, dataclasses with eval_str=True, your own __annotations__ walk — breaks on that module. On 3.14, PEP 649 makes annotations lazily evaluated, so the explosion happens on the first get_type_hints call rather than at import: later, and further from the cause.

Two more that are real and worth knowing:

  • Connection must be annotated bare. typeshed gives it type parameters with defaults, so both Connection and Connection[Req, Resp] satisfy mypy — but Connection is not subscriptable at runtime, so the parameterised spelling is another get_type_hints landmine. Same for sharedctypes.Synchronized[int].
  • mp.Pipe() infers Connection[Any, Any], so conn.recv() is a silent Any and everything downstream of it is unchecked.

Your task

The starter has eight functions with eight wrong annotations. Fix them so that both gates pass — mypy --strict with zero ignores, and typing.get_type_hints() succeeding on every one — then report where each type really lives:

def solve(*, functions: list[str]) -> dict[str, str]:

For each named function, resolve its obj annotation with typing.get_type_hints, normalise a generic alias to its origin with typing.get_origin, and return __module__.

The grader asks for subsets of the eight, so there is nowhere to hide a lookup table: the answer has to come out of the annotation you wrote.

The typing lesson

This problem is the clearest example in the track of a checker being necessary and not sufficient. mypy --strict is a static approximation of the runtime; where the runtime object is a bound method pretending to be a class, the approximation is simply wrong, and the only thing that finds it is executing get_type_hints — which is what your dependencies do on import.

Add that call to a test. It costs one line and it catches an entire class of annotation bug that no checker will ever report.

Loading visualization…