You have:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)

The main thread submits a task specifying worker function f1. Then the main thread exits the block and an implicit call to executor.shutdown() is made. Any tasks already submitted and have started to execute will complete but onceshutdown is called submitted tasks that have not yet started execution will be thrown away. In your code the call to shutdown occurs before worker function f1 has had a chance to submit the new task with f2 as the worker function and get its execution started. This can be demonstrated as follows:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    import time
    time.sleep(.1)

We have delayed the call to shutdown by .1 seconds giving f1 a chance to get f2 started. But even this has a race condition: Is .1 seconds always enough time to allow f1 to submit the second task and for that task to start? We cannot depend on this method.

TL;DR

You can skip to the final section Solution if you wish and not read the following solutions for simpler cases.

Attempts

To remove that race condition we can use a multithreading.Event that gets set only after all tasks that we need to submit have started executing:

import concurrent.futures
from threading import Event

all_tasks_submitted = Event()

def f2():
    all_tasks_submitted.set()
    print("hello, f2")
    return 3


def f1():
    print("hello, f1")
    print(executor.submit(f2).result())


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    all_tasks_submitted.wait()

Prints:

hello, f1
hello, f2
3

So now let's look at your actual case. First, there is a slight bug: f2 takes only two arguments but f1 is trying to invoke it with 3 arguments.

This is far more complicated case in that we are ultimately trying to start 10 * 10 * 10 = 1000 f3 tasks. So we now need a counter to keep track of how many f3 have been started:

import concurrent.futures
from threading import Event, Lock

all_tasks_started = Event()
lock = Lock()


NUM_F3_TASKS = 1_000
total_f3_tasks_started = 0

def f3(arg1, arg2):
    global total_f3_tasks_started

    with lock:
        total_f3_tasks_started += 1
        n = total_f3_tasks_started

    if n == NUM_F3_TASKS:
        all_tasks_started.set()

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        executor.submit(f3, arg2, i)


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        executor.submit(f2, arg2, arg3)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    for i in range(10):
        executor.submit(f1, i, 1, 2)
    all_tasks_started.wait()

Prints:

hello, f1 0
hello, f1 1
hello, f1 2
hello, f1 3
hello, f1 4
hello, f1 5
hello, f1 6
hello, f1 7
hello, f1 8
hello, f1 9
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 2, f3 tasks started = 993
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000

But this means that you need to know in advance exactly how many f3 tasks need to be created. You might be tempted to solve the problem by having f1 not return until all tasks it has submitted complete and having f2 not return until all tasks it has submitted complete. You would thus be having a 10 f1 tasks, 100 f2 tasks and 1000 f3 tasks running concurrently for which you would need a thread pool of size 1110.

Solution

We use an explicit task queue and a task executor as follows:

import concurrent.futures
from queue import Queue
from threading import Lock

task_queue = Queue()
lock = Lock()

task_number = 0

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        task_queue.put((f3, arg2, i))


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        task_queue.put((f2, arg2, arg3))


def pool_executor():
    while True:
        task = task_queue.get()
        if task is None:
            # sentinel to terminate
            return

        fn, *args = task
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

with concurrent.futures.ThreadPoolExecutor(POOL_SIZE) as executor:
    for _ in range(POOL_SIZE):
        executor.submit(pool_executor)

    for i in range(10):
        task_queue.put((f1, i, 1, 2))

    # Wait for all tasks to complete
    task_queue.join()
    # Now we need to terminate the running pool_executor tasks:
    # Add sentinels:
    for _ in range(POOL_SIZE):
        task_queue.put(None)

Prints:

hello, f1 0
hello, f1 1
hello, f1 3
hello, f1 5
hello, f1 7
hello, f1 9
hello, f1 2
hello, f2 1
hello, f2 1
hello, f2 1
hello, f1 4
hello, f1 6
hello, f1 8
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 1, task_number = 992
hello, f3, 2, 2, task_number = 993
hello, f3, 2, 4, task_number = 995
hello, f3, 2, 6, task_number = 997
hello, f3, 2, 8, task_number = 999
hello, f3, 2, 3, task_number = 994
hello, f3, 2, 7, task_number = 998
hello, f3, 2, 5, task_number = 996
hello, f3, 2, 9, task_number = 1000

Perhaps you should consider creating your own thread pool with dameon threads, which will terminate when the main process terminates (you could still use the technique of adding sentinel values to signal these threads to terminate when we no longer require them in which case the threads need not be daemon threads).

from queue import Queue
from threading import Lock, Thread

...

def pool_executor():
    while True:
        fn, *args = task_queue.get()
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

for _ in range(POOL_SIZE):
    Thread(target=pool_executor, daemon=True).start()

for i in range(10):
    task_queue.put((f1, i, 1, 2))

# Wait for all tasks to complete
task_queue.join()

A New Type of Multithreading Pool

We can abstract a multithreading pool that allows running tasks to continue to arbitrarily submit additional tasks and then be able to wait for all tasks to complete. That is, we wait until the task queue has quiesced, the condition where the task queue is empty and no new tasks will be added because there are no tasks currently running:

from queue import Queue
from threading import Thread

class ThreadPool:
    def __init__(self, pool_size):
        self._pool_size = pool_size
        self._task_queue = Queue()
        self._shutting_down = False
        for _ in range(self._pool_size):
            Thread(target=self._executor, daemon=True).start()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()

    def _terminate_threads(self):
        """Tell threads to terminate."""
        # No new tasks in case this is an immediate shutdown:
        self._shutting_down = True

        for _ in range(self._pool_size):
            self._task_queue.put(None)
        self._task_queue.join()  # Wait for all threads to terminate


    def shutdown(self, wait=True):
        if wait:
            # Wait until the task queue quiesces (becomes empty).
            # Running tasks may be continuing to submit tasks to the queue but
            # the expectation is that at some point no more tasks will be added
            # and we wait for the queue to become empty:
            self._task_queue.join()
        self._terminate_threads()

    def submit(self, fn, *args):
        if self._shutting_down:
            return
        self._task_queue.put((fn, args))

    def _executor(self):
        while True:
            task = self._task_queue.get()
            if task is None:  # sentinel
                self._task_queue.task_done()
                return
            fn, args = task
            try:
                fn(*args)
            except Exception as e:
                print(e)
            # Show this work has been completed:
            self._task_queue.task_done()

###############################################

from threading import Lock

lock = Lock()

task_number = 0

results = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    #print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")
    results.append(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    for i in range(10):
        pool.submit(f3, arg2, i)

def f1(arg1, arg2, arg3):
    for i in range(10):
        pool.submit(f2, arg2, arg3)


with ThreadPool(16) as pool:
    for i in range(10):
        pool.submit(f1, i, 1, 2)

for result in results:
    print(result)

Another Way That Uses Standard concurrent.futures Methods

As you have observed, in the above solution an f1 task will complete before the f2 tasks it has submitted has completed and f2 tasks will terminate before f3 tasks have terminated. The problem with your original code was due to a shutdown being implicitly called before all 1000 f3 tasks were submitted. We can prevent this premature shutdown from occuring by having each worker function return a list of Future instance whose results we await:

from concurrent.futures import ThreadPoolExecutor, Future
from threading import Lock

task_number = 0

lock = Lock()

futures = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    futures.extend(
        executor.submit(f3, arg2, i)
        for i in range(10)
    )


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    futures.extend(
        executor.submit(f2, arg2, arg3)
        for i in range(10)
    )


with ThreadPoolExecutor(16) as executor:
    futures.extend(
        executor.submit(f1, i, 1, 2)
        for i in range(10)
    )

    cnt = 0
    for future in futures:
        future.result()
        cnt += 1
    print(cnt, 'tasks completed.')

Prints:

...
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000
1110 tasks completed.
Answer from Booboo on Stack Overflow
🌐
Python
docs.python.org › 3 › library › concurrent.futures.html
concurrent.futures — Launching parallel tasks — Python 3.14 ...
January 30, 2026 - The executor may replace uncaught exceptions from initializer with ExecutionFailed. Other caveats from parent ThreadPoolExecutor apply here. submit() and map() work like normal, except the worker serializes the callable and arguments using pickle when sending them to its interpreter.
Top answer
1 of 3
2

You have:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)

The main thread submits a task specifying worker function f1. Then the main thread exits the block and an implicit call to executor.shutdown() is made. Any tasks already submitted and have started to execute will complete but onceshutdown is called submitted tasks that have not yet started execution will be thrown away. In your code the call to shutdown occurs before worker function f1 has had a chance to submit the new task with f2 as the worker function and get its execution started. This can be demonstrated as follows:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    import time
    time.sleep(.1)

We have delayed the call to shutdown by .1 seconds giving f1 a chance to get f2 started. But even this has a race condition: Is .1 seconds always enough time to allow f1 to submit the second task and for that task to start? We cannot depend on this method.

TL;DR

You can skip to the final section Solution if you wish and not read the following solutions for simpler cases.

Attempts

To remove that race condition we can use a multithreading.Event that gets set only after all tasks that we need to submit have started executing:

import concurrent.futures
from threading import Event

all_tasks_submitted = Event()

def f2():
    all_tasks_submitted.set()
    print("hello, f2")
    return 3


def f1():
    print("hello, f1")
    print(executor.submit(f2).result())


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    all_tasks_submitted.wait()

Prints:

hello, f1
hello, f2
3

So now let's look at your actual case. First, there is a slight bug: f2 takes only two arguments but f1 is trying to invoke it with 3 arguments.

This is far more complicated case in that we are ultimately trying to start 10 * 10 * 10 = 1000 f3 tasks. So we now need a counter to keep track of how many f3 have been started:

import concurrent.futures
from threading import Event, Lock

all_tasks_started = Event()
lock = Lock()


NUM_F3_TASKS = 1_000
total_f3_tasks_started = 0

def f3(arg1, arg2):
    global total_f3_tasks_started

    with lock:
        total_f3_tasks_started += 1
        n = total_f3_tasks_started

    if n == NUM_F3_TASKS:
        all_tasks_started.set()

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        executor.submit(f3, arg2, i)


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        executor.submit(f2, arg2, arg3)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    for i in range(10):
        executor.submit(f1, i, 1, 2)
    all_tasks_started.wait()

Prints:

hello, f1 0
hello, f1 1
hello, f1 2
hello, f1 3
hello, f1 4
hello, f1 5
hello, f1 6
hello, f1 7
hello, f1 8
hello, f1 9
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 2, f3 tasks started = 993
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000

But this means that you need to know in advance exactly how many f3 tasks need to be created. You might be tempted to solve the problem by having f1 not return until all tasks it has submitted complete and having f2 not return until all tasks it has submitted complete. You would thus be having a 10 f1 tasks, 100 f2 tasks and 1000 f3 tasks running concurrently for which you would need a thread pool of size 1110.

Solution

We use an explicit task queue and a task executor as follows:

import concurrent.futures
from queue import Queue
from threading import Lock

task_queue = Queue()
lock = Lock()

task_number = 0

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        task_queue.put((f3, arg2, i))


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        task_queue.put((f2, arg2, arg3))


def pool_executor():
    while True:
        task = task_queue.get()
        if task is None:
            # sentinel to terminate
            return

        fn, *args = task
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

with concurrent.futures.ThreadPoolExecutor(POOL_SIZE) as executor:
    for _ in range(POOL_SIZE):
        executor.submit(pool_executor)

    for i in range(10):
        task_queue.put((f1, i, 1, 2))

    # Wait for all tasks to complete
    task_queue.join()
    # Now we need to terminate the running pool_executor tasks:
    # Add sentinels:
    for _ in range(POOL_SIZE):
        task_queue.put(None)

Prints:

hello, f1 0
hello, f1 1
hello, f1 3
hello, f1 5
hello, f1 7
hello, f1 9
hello, f1 2
hello, f2 1
hello, f2 1
hello, f2 1
hello, f1 4
hello, f1 6
hello, f1 8
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 1, task_number = 992
hello, f3, 2, 2, task_number = 993
hello, f3, 2, 4, task_number = 995
hello, f3, 2, 6, task_number = 997
hello, f3, 2, 8, task_number = 999
hello, f3, 2, 3, task_number = 994
hello, f3, 2, 7, task_number = 998
hello, f3, 2, 5, task_number = 996
hello, f3, 2, 9, task_number = 1000

Perhaps you should consider creating your own thread pool with dameon threads, which will terminate when the main process terminates (you could still use the technique of adding sentinel values to signal these threads to terminate when we no longer require them in which case the threads need not be daemon threads).

from queue import Queue
from threading import Lock, Thread

...

def pool_executor():
    while True:
        fn, *args = task_queue.get()
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

for _ in range(POOL_SIZE):
    Thread(target=pool_executor, daemon=True).start()

for i in range(10):
    task_queue.put((f1, i, 1, 2))

# Wait for all tasks to complete
task_queue.join()

A New Type of Multithreading Pool

We can abstract a multithreading pool that allows running tasks to continue to arbitrarily submit additional tasks and then be able to wait for all tasks to complete. That is, we wait until the task queue has quiesced, the condition where the task queue is empty and no new tasks will be added because there are no tasks currently running:

from queue import Queue
from threading import Thread

class ThreadPool:
    def __init__(self, pool_size):
        self._pool_size = pool_size
        self._task_queue = Queue()
        self._shutting_down = False
        for _ in range(self._pool_size):
            Thread(target=self._executor, daemon=True).start()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()

    def _terminate_threads(self):
        """Tell threads to terminate."""
        # No new tasks in case this is an immediate shutdown:
        self._shutting_down = True

        for _ in range(self._pool_size):
            self._task_queue.put(None)
        self._task_queue.join()  # Wait for all threads to terminate


    def shutdown(self, wait=True):
        if wait:
            # Wait until the task queue quiesces (becomes empty).
            # Running tasks may be continuing to submit tasks to the queue but
            # the expectation is that at some point no more tasks will be added
            # and we wait for the queue to become empty:
            self._task_queue.join()
        self._terminate_threads()

    def submit(self, fn, *args):
        if self._shutting_down:
            return
        self._task_queue.put((fn, args))

    def _executor(self):
        while True:
            task = self._task_queue.get()
            if task is None:  # sentinel
                self._task_queue.task_done()
                return
            fn, args = task
            try:
                fn(*args)
            except Exception as e:
                print(e)
            # Show this work has been completed:
            self._task_queue.task_done()

###############################################

from threading import Lock

lock = Lock()

task_number = 0

results = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    #print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")
    results.append(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    for i in range(10):
        pool.submit(f3, arg2, i)

def f1(arg1, arg2, arg3):
    for i in range(10):
        pool.submit(f2, arg2, arg3)


with ThreadPool(16) as pool:
    for i in range(10):
        pool.submit(f1, i, 1, 2)

for result in results:
    print(result)

Another Way That Uses Standard concurrent.futures Methods

As you have observed, in the above solution an f1 task will complete before the f2 tasks it has submitted has completed and f2 tasks will terminate before f3 tasks have terminated. The problem with your original code was due to a shutdown being implicitly called before all 1000 f3 tasks were submitted. We can prevent this premature shutdown from occuring by having each worker function return a list of Future instance whose results we await:

from concurrent.futures import ThreadPoolExecutor, Future
from threading import Lock

task_number = 0

lock = Lock()

futures = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    futures.extend(
        executor.submit(f3, arg2, i)
        for i in range(10)
    )


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    futures.extend(
        executor.submit(f2, arg2, arg3)
        for i in range(10)
    )


with ThreadPoolExecutor(16) as executor:
    futures.extend(
        executor.submit(f1, i, 1, 2)
        for i in range(10)
    )

    cnt = 0
    for future in futures:
        future.result()
        cnt += 1
    print(cnt, 'tasks completed.')

Prints:

...
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000
1110 tasks completed.
2 of 3
0

Looks like, in your first example, maybe the program terminates before the "f2" thread gets its chance to print.

Adding a time.sleep call to the very end of your first example allows it to print both "hello" messages when I run it.*

import concurrent.futures
import time

def f2():
    print("hello, f2")


def f1():
    print("hello, f1")
    executor.submit(f2)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    time.sleep(3)

I have not ever used concurrent.futures. Are its worker threads daemons?†


Update:

I wonder why main thread cannot find the f2 future...

Not sure what you're asking, but try this:

import concurrent.futures
import queue

q = queue.Queue(2)

def f2():
    print("hello, f2")

def f1():
    print("hello, f1")
    future_2 = executor.submit(f2)
    q.put(future_2)

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    future_1 = executor.submit(f1)
    future_2 = q.get()
    future_1.result()
    future_2.result()

* Python 3.12.4 running on macOS Sonoma 14.4.1.

† I can't find "daemon" or "demon" anywhere in The documentation, and I can't find out by using Threading.current_thread().daemon because the current_thread function only works in threads that were created directly by the Threading module. current_thread returns a bogus object when called by a concurrent.futures worker thread.

Discussions

dictionary - Python concurrent executor.map() and submit() - Stack Overflow
123 How does ThreadPoolExecutor().map differ from ThreadPoolExecutor().submit? 5 Python concurrent futures executor.submit timeout More on stackoverflow.com
🌐 stackoverflow.com
python - passing multiple arguments to a thread pool executor - Stack Overflow
I am writing a code to run pool executor and use a function with two arguments. More on stackoverflow.com
🌐 stackoverflow.com
python - Can I submit the task to ProcessPoolExecutor in "class instance"? - Stack Overflow
Instead, I want the print_time task to be submitted to the executor whenever update_time updates the time within the class. ... import time import threading import concurrent.futures class time_print_class: def __init__(self, tid, executor): self.tid = tid self.executor = executor time_thread ... More on stackoverflow.com
🌐 stackoverflow.com
Sending multiple arguments to ProcessPoolExecutor() executor
I've been messing around with multiprocessing in my research -- sometimes it works sometimes it's slower. I like the idea of the… More on reddit.com
🌐 r/learnpython
1
1
March 9, 2022
🌐
Super Fast Python
superfastpython.com › home › tutorials › how to use threadpoolexecutor submit()
How to use ThreadPoolExecutor submit() - Super Fast Python
August 3, 2023 - You can issue one-off tasks to the ThreadPoolExecutor using the submit() method. This returns a Future object that gives control over the asynchronous task executed in the thread pool.
🌐
Python for Network Engineers
pyneng.readthedocs.io › en › latest › book › 19_concurrent_connections › concurrent_futures_submit.html
Method submit and work with futures - Python for network engineers
Creation of list with Future can be done with list comprehensions: future_list = [executor.submit(send_show, device, 'sh clock') for device in devices] The result is: $ python netmiko_threads_submit_basics.py ThreadPoolExecutor-0_0 root INFO: ===> 17:32:59.088025 Connection: 192.168.100.1 ThreadPoolExecutor-0_1 root INFO: ===> 17:32:59.094103 Connection: 192.168.100.2 ThreadPoolExecutor-0_1 root INFO: <=== 17:33:11.639672 Received: 192.168.100.2 {'192.168.100.2': '*17:33:11.429 UTC Thu Jul 4 2019'} ThreadPoolExecutor-0_1 root INFO: ===> 17:33:11.849132 Connection: 192.168.100.3 ThreadPoolExecu
🌐
GeeksforGeeks
geeksforgeeks.org › python › how-to-use-threadpoolexecutor-in-python3
How to use ThreadPoolExecutor in Python3 ? - GeeksforGeeks
July 23, 2025 - ThreadPoolExecutor class exposes ... below. submit(fn, *args, **kwargs): It runs a callable or a method and returns a Future object representing the execution state of the method....
Top answer
1 of 2
2

Here is the map version of your existing code. Note that the callback now accepts a tuple as a parameter. I added an try\except in the callback so the results will not throw an error. The results are ordered according to the input list.

from concurrent.futures import ThreadPoolExecutor
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://www.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the url and contents
def load_url(tt):  # (url,timeout)
    url, timeout = tt
    try:
      with urllib.request.urlopen(url, timeout=timeout) as conn:
         return (url, conn.read())
    except Exception as ex:
        print("Error:", url, ex)
        return(url,"")  # error, return empty string

with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(load_url, [(u,60) for u in URLS])  # pass url and timeout as tuple to callback
    executor.shutdown(wait=True) # wait for all complete
    print("Results:")
for r in results:  # ordered results, will throw exception here if not handled in callback
    print('   %r page is %d bytes' % (r[0], len(r[1])))

Output

Error: http://www.wsj.com/ HTTP Error 404: Not Found
Results:
   'http://www.foxnews.com/' page is 320028 bytes
   'http://www.cnn.com/' page is 1144916 bytes
   'http://www.wsj.com/' page is 0 bytes
   'http://www.bbc.co.uk/' page is 279418 bytes
   'http://some-made-up-domain.com/' page is 64668 bytes
2 of 2
1

Without using the map method, you can use enumerate to build the future_to_url dict with not just the URLs as values, but also their indices in the list. You can then build a dict from the future objects returned by the call to concurrent.futures.as_completed(future_to_url) with indices as the keys, so that you can iterate an index over the length of the dict to read the dict in the same order as the corresponding items in the original list:

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {
        executor.submit(load_url, url, 60): (i, url) for i, url in enumerate(URLS)
    }
    futures = {}
    for future in concurrent.futures.as_completed(future_to_url):
        i, url = future_to_url[future]
        futures[i] = url, future
    for i in range(len(futures)):
        url, future = futures[i]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))
🌐
DigitalOcean
digitalocean.com › community › tutorials › how-to-use-threadpoolexecutor-in-python-3
How To Use ThreadPoolExecutor in Python 3 | DigitalOcean
June 23, 2020 - A with statement is used to create a ThreadPoolExecutor instance executor that will promptly clean up threads upon completion. Four jobs are submitted to the executor: one for each of the URLs in the wiki_page_urls list.
Find elsewhere
🌐
pytz
pythonhosted.org › futures
concurrent.futures — Asynchronous computation — futures 2.1.3 documentation
def wait_on_future(): f = executor.submit(pow, 5, 2) # This will never complete because there is only one worker thread and # it is executing this function.
🌐
Alexwlchan
alexwlchan.net › 2019 › adventures-with-concurrent-futures
Adventures in Python with concurrent.futures – alexwlchan
import concurrent.futures with concurrent.futures.Executor() as executor: futures = { executor.submit(perform, task): task for task in get_tasks_to_do() } for fut in concurrent.futures.as_completed(futures): original_task = futures[fut] print(f"The outcome of {original_task} is {fut.result()}") Rather than creating a set, we’re creating a dict that maps each Future to its original task. When the Future completes, we look in the dict to find the task. There’s a concrete example of this pattern in the Python docs.
🌐
Medium
medium.com › towardsdev › whats-the-best-way-to-handle-concurrency-in-python-threadpoolexecutor-or-asyncio-85da1be58557
What’s the Best Way to Handle Concurrency in Python: ThreadPoolExecutor or asyncio? | by Soumit Salman Rahman | Towards Dev
July 28, 2025 - from concurrent.futures import ThreadPoolExecutor import time def log_message(message): # do stuff print(f"Logging: {message}") def send_notification(user): # do other stuff print(f"Notifying: {user}") def run(): with ThreadPoolExecutor(max_workers=2) as executor: executor.submit(log_message, "System started") executor.submit(send_notification, "Alice") # the context waits for all the submitted tasks to finish print("All tasks done")
🌐
ZetCode
zetcode.com › python › threadpoolexecutor
Python ThreadPoolExecutor - concurrency in Python with ThreadPoolExecutor
#!/usr/bin/python from time import sleep, perf_counter import random from concurrent.futures import ThreadPoolExecutor def task(tid): r = random.randint(1, 5) print(f'task {tid} started, sleeping {r} secs') sleep(r) return f'finished task {tid}, slept {r}' start = perf_counter() with ThreadPoolExecutor() as executor: t1 = executor.submit(task, 1) t2 = executor.submit(task, 2) t3 = executor.submit(task, 3) print(t1.result()) print(t2.result()) print(t3.result()) finish = perf_counter() print(f"It took {finish-start} second(s) to finish.") This example runs tasks that sleep for random durations, retrieves their results, and measures total execution time using perf_counter.
🌐
TutorialEdge
tutorialedge.net › python › concurrency › python-threadpoolexecutor-tutorial
Python ThreadPoolExecutor Tutorial | TutorialEdge.net
October 1, 2017 - Context managers, if you haven’t encountered them before are an incredibly powerful concept with Python that allow us to write more syntactically beautiful code. This time we’ll be defining a different task that takes in a variable ‘n’ as input just to give you a simple demonstration of how we can do this. The task function just prints out that it’s processing ‘n’ and nothing more. Within our main function we utilize our ThreadPoolExecutor as a context manager and then call future = executor.submit(task, (n)) 3 times in order to give our threadpool something to do.
🌐
Medium
blog.wahab2.com › python-threadpoolexecutor-use-cases-for-parallel-processing-3d5c90fd5634
Python ThreadPoolExecutor: Use Cases for Parallel Processing | by Abdul Rafee Wahab | Medium
August 23, 2023 - We will create a pool of 4 worker threads and submit each computation task to the pool using the submit() function. import concurrent.futures def fibonacci(n): if n <= 1: return n else: return fibonacci(n-1) + fibonacci(n-2) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: # Submit each computation task to the thread pool futures = [executor.submit(fibonacci, n) for n in range(20)] # Wait for all tasks to complete and retrieve the results results = [future.result() for future in concurrent.futures.as_completed(futures)] # Print the results print(results)
🌐
Python Engineer
python-engineer.com › posts › threadpoolexecutor
How to use ThreadPoolExecutor in Python - Python Engineer
May 2, 2022 - with ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(pow, 2, 15) print(future.result())
🌐
Python Tutorial
pythontutorial.net › home › python concurrency › python threadpoolexecutor
Python ThreadPoolExecutor By Practical Examples
July 15, 2022 - The Executor class has three methods to control the thread pool: submit() – dispatch a function to be executed and return a Future object. The submit() method takes a function and executes it asynchronously. map() – execute a function ...
🌐
Python Module of the Week
pymotw.com › 3 › concurrent.futures
concurrent.futures — Manage Pools of Concurrent Tasks
In addition to using map(), it is possible to schedule an individual task with an executor using submit(), and use the Future instance returned to wait for that task’s results.
🌐
Reddit
reddit.com › r/learnpython › sending multiple arguments to processpoolexecutor() executor
r/learnpython on Reddit: Sending multiple arguments to ProcessPoolExecutor() executor
March 9, 2022 - import concurrent.futures from time import sleep, time def some_function(_a, _b, _c): sleep(1) return (_a + _b) * _c def some_function_wrapper(args): return some_function(*args) def main(): st = time() print(f"Starting...") with concurrent.futures.ProcessPoolExecutor() as _executor: pool = [ _executor.submit(some_function_wrapper, args=(a, b, c)) for a in range(3) for b in range(3) for c in range(3) ] k = 0 for result in concurrent.futures.as_completed(pool): print(f"{k}\t{result.result()}") k += 1 print(f"Fin t={time() - st}") if __name__ == "__main__": main() ... D:\PythonProjects\testBench\venv\Scripts\python.exe D:/PythonProjects/testBench/main.py Starting...