Concurrent.futures ProcessPoolExecutor Example does not work in Python 3.14

The example code for the ProcessPoolExecutor listed on the concurrent.futures page does not actually run on Python 3.14 (I only tested on Linux). This seems to be caused by the change of the start method away from fork as mentioned in a change notice further up on the page. I must say I don’t fully understand why the code would not work and find it a bit odd. Changing the start method to fork fixes the issue.

Here is a quick example I used to test:

podman run -it docker.io/python:3.14 python3

(change to docker or run with you local interpreter,I just wanted to exclude that it is caused by my distributions Python install)

Copy-and-paste the example code:

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()

This will raise a AttributeError: module '__main__' has no attribute 'is_prime'.

Full error message
Process SpawnProcess-4:
Traceback (most recent call last):
  File "/usr/local/lib/python3.14/multiprocessing/process.py", line 320, in _bootstrap
    self.run()
    ~~~~~~~~^^
  File "/usr/local/lib/python3.14/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/concurrent/futures/process.py", line 242, in _process_worker
    call_item = call_queue.get(block=True)
  File "/usr/local/lib/python3.14/multiprocessing/queues.py", line 120, in get
    return _ForkingPickler.loads(res)
           ~~~~~~~~~~~~~~~~~~~~~^^^^^
AttributeError: module '__main__' has no attribute 'is_prime'
Process SpawnProcess-1:
Traceback (most recent call last):
Traceback (most recent call last):
  File "<python-input-2>", line 32, in <module>
    main()
    ~~~~^^
  File "<python-input-2>", line 28, in main
    for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
                         ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/concurrent/futures/process.py", line 712, in _chain_from_iterable_of_lists
    for element in iterable:
                   ^^^^^^^^
  File "/usr/local/lib/python3.14/concurrent/futures/_base.py", line 645, in result_iterator
    yield _result_or_cancel(fs.pop())
          ~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/usr/local/lib/python3.14/concurrent/futures/_base.py", line 312, in _result_or_cancel
    return fut.result(timeout)
           ~~~~~~~~~~^^^^^^^^^
  File "/usr/local/lib/python3.14/concurrent/futures/_base.py", line 454, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.14/concurrent/futures/_base.py", line 396, in __get_result
    raise self._exception
concurrent.futures.process.BrokenProcessPool: A process in the process pool was terminated abruptly while the future was running or pending.

Python 3.13 runs this without said exception. To fix the code under 3.14, you would have to force the start method to fork:

import multiprocessing

...
with concurrent.futures.ProcessPoolExecutor(mp_context=multiprocessing.get_context("fork")) as executor:
    ...
Full code snippet of the working example
import concurrent.futures
import math
import multiprocessing

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor(mp_context=multiprocessing.get_context("fork")) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()

I’m not so sure if the example code should be changed to use the fork method, but I also don’t understand why this code would fail for methods other then fork. (Maybe this thread should go in the Help category?) Can someone maybe shine some light onto why it fails? And I guess the documentation should be updated accordingly?

Are you running it from an interactive REPL command prompt? Docs:

The __main__ module must be importable by worker subprocesses. This means that ProcessPoolExecutor will not work in the interactive interpreter.

1 Like

Yes, I ran it from the REPL.
I see, that makes a lot of sense. I initially came across this problem when running something similar in a Jupyter Notebook, which I guess also works more like a REPL than a Python module. What threw me off was the difference in behavior between 3.13 and 3.14.

Thanks for clarifying! :slight_smile: