Back to Blog
Python

Python Future Result: Retrieving Values from Concurrent Tasks

python future result: Learn how to retrieve results from Python Future objects, handle exceptions, set timeouts, and use callbacks in concurrent.futures.

concurrent.futuresFuturethreadingexception handling
A Python Future object with a result value and a checkmark, representing successful completion of a concurrent task.

When you need a python future result, you typically work with the Future object from the concurrent.futures module. A Future represents the eventual outcome of an asynchronous operation, and it provides a consistent interface to check status, retrieve the value, or handle errors.

What a Future Represents in Python

A Future is a placeholder for a result that will be available at some point. It is returned by executor.submit() and by map() in concurrent.futures. The Future object tracks the state of the underlying callable: whether it is running, completed, or cancelled. This abstraction lets you write code that does not block until the result is actually needed.

Retrieving the Result with result()

The most common way to get a python future result is to call result() on the Future. This method blocks until the callable completes and returns the value. If the callable raised an exception, result() re-raises that exception.

from concurrent.futures import ThreadPoolExecutor def square(n): return n * n with ThreadPoolExecutor() as executor: future = executor.submit(square, 5) result = future.result() print(result) # 25

The result() method accepts an optional timeout parameter. If the future does not complete within that many seconds, it raises concurrent.futures.TimeoutError.

Handling Exceptions from a Future

When a callable raises an exception, the Future captures it. Calling result() will re-raise the original exception. You can also inspect it with exception() without raising it.

def fail(): raise ValueError("bad input") with ThreadPoolExecutor() as executor: future = executor.submit(fail) try: future.result() except ValueError as e: print(f"Caught: {e}") # Alternatively, inspect the exception object if future.exception() is not None: print("Future failed")

The exception() method returns the exception instance if the future failed, or None if it completed successfully or was cancelled.

Using done() and add_done_callback()

Instead of blocking on result(), you can register a callback that runs when the future completes. The callback receives the Future object. This is useful for non-blocking workflows.

def on_done(fut): print(f"Result: {fut.result()}") with ThreadPoolExecutor() as executor: future = executor.submit(square, 7) future.add_done_callback(on_done)

Note that the callback runs in the thread that completes the future, so it may block that worker if it does heavy work. If you need to run callbacks in the main thread, you will need to coordinate with an event loop or a queue.

Cancelling a Future and Its Consequences

A future can be cancelled only if it has not started running. Calling cancel() returns True if the future was cancelled, False otherwise. If cancelled, result() raises concurrent.futures.CancelledError.

future = executor.submit(square, 10) print(future.cancel()) # True if not started

Once a future is running, cancel() returns False and the operation continues. Cancelling a future that is already done has no effect.

Timeouts and Partial Results

When using result(timeout=...), you must handle TimeoutError. The future continues running in the background; you can try again later.

import time def slow(): time.sleep(5) return 42 with ThreadPoolExecutor() as executor: future = executor.submit(slow) try: value = future.result(timeout=2) except concurrent.futures.TimeoutError: print("Timed out, will wait longer") value = future.result() # blocks until done

A timeout does not cancel the underlying task. It only stops the current call from waiting further.

Choosing Between ThreadPoolExecutor and ProcessPoolExecutor

Both executors return Future objects, but they differ in how work is executed. ThreadPoolExecutor is suitable for I/O-bound tasks; ProcessPoolExecutor for CPU-bound tasks. The result() and exception() behavior is identical, but pickling requirements differ for process pools.

FeatureThreadPoolExecutorProcessPoolExecutor
Best forI/O-bound tasksCPU-bound tasks
Memory sharingShared memorySeparate processes
PicklingNot requiredRequired for arguments and results

Use ThreadPoolExecutor when your tasks spend most of their time waiting on network, disk, or user input. Use ProcessPoolExecutor when your tasks are compute-heavy and need to bypass the GIL.

Common Pitfalls with Future Results

Calling result() on a future that is already done returns immediately. Calling it multiple times is safe, but if the future raised an exception, it will re-raise each time. Also, be careful not to block the main thread indefinitely without a timeout in production code.

When using map(), you get an iterator of results, not futures. If you need to handle exceptions per item, use submit() with a list of futures. For example, to collect results while preserving order and handling failures individually, you can create a list of futures and iterate over them.

futures = [executor.submit(square, i) for i in range(10)] for fut in futures: try: print(fut.result()) except Exception as e: print(f"Task failed: {e}")

This pattern gives you fine-grained control over each task's outcome, which is often necessary in real applications.

python future result: Practical Usage and Code Examples | RYUSLOG DEV