Python 3.14 Concurrency & Threading Reference
Source: Python 3.14.6 documentation (docs.python.org, last updated 2026-06-10), read in full:
threading, concurrent.futures, concurrent.interpreters, the Concurrent Execution
chapter index, and the Python support for free threading HOWTO. Written for
orchestrata-ultimate, which runs Python 3.14t (free-threaded, GIL genuinely off).
The four concurrency models
| Model | Module | Parallel CPU? | Memory model | When |
|---|---|---|---|---|
| Threads | threading / ThreadPoolExecutor |
GIL build: no. 3.14t: yes | Shared, everything mutable | I/O-bound; CPU-bound too on 3.14t |
| Subinterpreters | concurrent.interpreters / InterpreterPoolExecutor |
Yes (per-interpreter GIL since 3.12) | Isolated; explicit sharing (pickle / Queue / memoryview) | CSP/actor-style isolation in one process |
| Processes | multiprocessing / ProcessPoolExecutor |
Yes | Isolated; pickle over pipes | CPU-bound on GIL builds; crash isolation |
| Event loop | asyncio |
No (single thread) | Shared, cooperative | High-fanout I/O |
threading — the primitives (3.14 state)
- Thread(group, target, name, args, kwargs, *, daemon, context) —
contextis NEW in 3.14: controls thecontextvars.Contextthe thread starts with. Default followssys.flags.thread_inherit_context: true on free-threaded builds (thread inherits a copy of the caller's context), false otherwise.start()now also sets the OS thread name (truncated: 15 bytes Linux).join()in late finalization can raisePythonFinalizationError(3.14). Daemon threads are killed abruptly at shutdown — use a non-daemon thread +Eventfor graceful stops. - Lock — lowest-level primitive; any thread may
release().acquire(blocking, timeout)is signal-interruptible on POSIX and (new in 3.14) on Windows.locked()query. - RLock — reentrant, owner-only release, recursion counter.
locked()is NEW in 3.14. Use as context manager; unbalanced acquire/release deadlocks. - Condition(lock=None) — wraps an RLock by default;
wait()/wait_for(predicate)must be called with the lock held; always re-check the predicate in a loop (spurious/late wakeups are inherent).notify(n)may wake more than n in future implementations.locked()NEW in 3.14. - Semaphore / BoundedSemaphore — counter;
release(n=1)can wake multiple (3.9+). Bounded raisesValueErroron over-release — use it for fixed-capacity resources. - Event —
set()/clear()/wait(timeout); simplest cross-thread signal. - Timer(interval, fn) — Thread subclass;
cancel()works only before the action starts. - Barrier(parties, action, timeout) — all
wait()then all released;wait()returns a unique index 0..parties-1 (use index 0 for one-time housekeeping); broken barrier raisesBrokenBarrierError; prefer a constructor timeout overabort(). - threading.local — per-thread attribute namespace; subclass
__init__re-runs per thread;__slots__on a local subclass is shared, not thread-local (trap). - Module functions:
active_count(),enumerate(),current_thread(),main_thread(),get_ident()(recyclable),get_native_id()(kernel TID),excepthook(uncaughtThread.run()exceptions;SystemExitsilently ignored),settrace_all_threads()/setprofile_all_threads()(3.12+),stack_size(),TIMEOUT_MAX. - All acquire/release objects are context managers — always prefer
with lock:.
Free-threading (PEP 703) — the 3.14t build
- Detect:
sys.versioncontains "free-threading build";sys._is_gil_enabled()for the running process;sysconfig.get_config_var("Py_GIL_DISABLED") == 1for the build. - The GIL can be re-enabled at runtime by
PYTHON_GIL=1,-X gil=1, or automatically by importing a C extension not marked free-threading-safe (prints a warning). This is why the project vendors FT builds of grpcio/msgpack/janus-swi and the conftest gates GIL-on. - Thread safety: builtins (
dict,list,set) use internal locks that mimic GIL-era behaviour — but that is an implementation description, not a guarantee. Use explicitthreading.Lockfor invariants. - Known limitations:
frame.f_localsof a frame executing in another thread may crash; sharing one iterator across threads may duplicate/drop elements; 1–8% single-thread overhead (pyperformance: ~1% macOS aarch64 → ~8% x86-64 Linux). - Immortalization: code constants and
sys.intern()-ed strings are immortal (never freed; refcounts untouched) to avoid contention. - Memory behaviour (why 3.14t RSS runs higher):
- Non-GC objects carry a bigger header (None: 32B vs 16B on AMD64).
- QSBR (quiescent-state-based reclamation) defers frees backing lock-free structures
(list, dict-keys);
gc.collect()flushes it. - mimalloc replaces pymalloc, with 4 separate heaps (free memory in one heap is
unusable by another);
MIMALLOC_PURGE_DELAY=0returns memory to the OS faster at a performance cost. - Biased refcounting (owner-thread fast path) + deferred refcounting (modules,
top-level functions, class methods, descriptors,
threading.localobjects — stack refs uncounted, freed by GC) + per-thread refcounting (heap types, code objects, module__dict__— freed only after counts merge at a safe point). Net effect: objects live longer than refcount-zero;gc.collect()reconciles.
- Behavioural deltas:
thread_inherit_contextandcontext_aware_warningsdefault true on 3.14t —warnings.catch_warningsbecomes context-local (thread-safe) instead of mutating the global filter list.
concurrent.futures
One abstract Executor API — submit(fn, *args) → Future, map(fn, *iterables, timeout, chunksize, buffersize), shutdown(wait, cancel_futures) — with three pools:
- ThreadPoolExecutor(max_workers, thread_name_prefix, initializer, initargs) — default
workers
min(32, process_cpu_count() + 4); reuses idle threads; all threads joined before interpreter exit (beforeatexithandlers — don't use for long-running tasks). Classic deadlock: a submitted callable waiting on another Future from the same (full) pool. - InterpreterPoolExecutor — NEW in 3.14. ThreadPoolExecutor subclass where each worker
thread owns its own subinterpreter → own GIL → true multi-core parallelism with in-process
cost. Callables, args, initializer, and return values cross the boundary via pickle;
each worker interpreter must re-import what it needs (fully separate module objects,
including
sysand__main__). Uncaught worker exceptions are preserved as-is with__cause__set to anExecutionFailedsummary. Failure class:BrokenInterpreterPool. - ProcessPoolExecutor(max_workers, mp_context, initializer, initargs, max_tasks_per_child) —
3.14 change: default start method is no longer fork (pass
mp_context=multiprocessing.get_context("fork")to keep it). NEW 3.14:terminate_workers()/kill_workers(). Windows cap 61.map(chunksize=)matters here only. map(buffersize=)is NEW in 3.14 — lazy iterable consumption with a bounded in-flight buffer.- Module functions:
wait(fs, timeout, return_when=FIRST_COMPLETED|FIRST_EXCEPTION|ALL_COMPLETED)→(done, not_done);as_completed(fs, timeout)— yields in completion order. - Future:
cancel/cancelled/running/done/result(timeout)/exception(timeout)/add_done_callback; test/executor-sideset_running_or_notify_cancel/set_result/set_exception(InvalidStateErrorif already done).
concurrent.interpreters (PEP 734) — NEW in 3.14
- Key frame: an interpreter is the execution context, not a thread. Isolation only — no implicit concurrency; combine with threads for parallelism ("threads with opt-in sharing", CSP/actor-model style). Per-interpreter GIL since 3.12.
- API:
create(),list_all(),get_current(),get_main(),create_queue().Interpreter:.id,.whence,.is_running(),.close(),.prepare_main(**kwargs)(bind objects into the target's__main__),.exec(code)(runs in current thread, globals = target's__main__),.call(fn, *args, **kwargs)(returns result),.call_in_thread(fn, ...)(returns the Thread). - Sharing: objects cross by pickle, EXCEPT immortal/immutable builtins passed efficiently
(
None,bool,bytes,str,int,float, tuples thereof) and two genuinely-shared mutable types:memoryviewand the cross-interpreterQueue(queue.Queue interface;QueueEmptyError/QueueFullErrorsubclass queue.Empty/Full). Unshareable →NotShareableError(TypeError subclass). - Exceptions:
InterpreterError,InterpreterNotFoundError,ExecutionFailed(carries.excinfosnapshot of the remote exception). - NOT a security boundary — same-process memory; extension modules can violate isolation.
- Extension modules must support multi-phase init (see "Isolating Extension Modules" HOWTO); many PyPI packages don't yet.
orchestrata-ultimate guidance
- We already have GIL-off threads (3.14t), so
InterpreterPoolExecutorbuys isolation, not extra parallelism, here. Its per-worker import state and pickle boundary make it a candidate for the test executor (module isolation without subprocess spawn cost) — registered as an evaluation work item; the pickle/import constraints vs. our per-module subprocess model are the open question. - The suite's parallelism doctrine stays: one pytest subprocess per module batch via
run_tests.py(RUL-005), never xdist (RUL-001). - 3.14t traps to keep respecting: no cross-thread iterator sharing, no
f_localspoking of live frames, explicit locks over builtin-internal locking,gc.collect()before asserting memory release (QSBR/deferred refcounts), and never import a non-FT extension (it silently re-enables the GIL) — the conftest GIL gate exists for exactly this.
Related: Free-threaded Prolog, Running the test suite, Pytest live observer.