Skip to content

Release 1.1.1: document how thread nesting differs by Python build - #24

Merged
seba2390 merged 1 commit into
mainfrom
docs/thread-context-by-build
Sep 30, 2026
Merged

seba2390 merged 1 commit into
mainfrom
docs/thread-context-by-build

Conversation

@seba2390

Copy link
Copy Markdown
Owner

What does this change?

The bug. On free-threaded Python 3.14 (3.14t), a new thread starts in a copy of the context that started it. ThreadPoolExecutor starts its workers lazily and reuses them, so each worker keeps the section it started under for as long as it lives. Work submitted later from a different section gets reported under the first one:

pool = ThreadPoolExecutor(max_workers=1)
with TimerContext("phase_a"):
    pool.submit(work).result()   # worker starts here
with TimerContext("phase_b"):
    pool.submit(work).result()   # same worker, reused
3.13  : [('phase_a',), ('phase_b',), ('work',)]
3.14  : [('phase_a',), ('phase_b',), ('work',)]
3.14t : [('phase_a',), ('phase_a', 'work'), ('phase_b',)]   ← phase_b's call filed under phase_a

Why only docs and tests. The library can't tell a context a thread was meant to inherit from a stale one, so it can't fix this itself. What was wrong was the guide. It said pool work is always recorded at the top level, in a comment the docs test never asserted and that is false on 3.14t. It also said free-threaded builds nest thread work "without extra work", which is only true for a brand-new plain thread.

Docs (docs/guide/concurrency.md, Threads section):

  • States plainly which builds inherit the context, and mentions -X thread_inherit_context / sys.flags.thread_inherit_context.
  • Explains why pools make the difference matter.
  • Gives two recipes that produce the same report on every build: pool.submit(contextvars.copy_context().run, fn, ...) to nest under the current section, and contextvars.Context().run to keep work at the top level. Both examples assert their results, and the pool is reused across two sections to show the first recipe copes with reuse.
  • Says to copy the context for each call. The old example copied it once, which breaks as soon as a reader uses it with pool.map: a context can't be entered by two threads at once.

Tests (4 new ones in TestThreading): how a plain thread nests and how a reused pool worker nests, each pinned per build via sys.flags.thread_inherit_context, plus both recipes checked with no branching on the build.

Release: bumps __version__ to 1.1.1, adds the [1.1.1] changelog section under ### Documentation, and updates the bug template's version placeholder. There's no API or behaviour change, so this is a patch release.

Checklist

  • uv run pytest --cov passes (124 tests, 100% statement and branch coverage). It also passes locally on 3.11, 3.12, 3.13, 3.14 and 3.14t, and the thread tests pass on 3.14 with -X thread_inherit_context=1 and on 3.14t with =0, so both expectations are exercised on both builds.
  • uv run ruff check and uv run ruff format --check pass
  • uv run basedpyright passes (0 errors, 0 warnings)
  • Behaviour is pinned by tests (no library code changed)
  • CHANGELOG.md updated
  • uv lock --check, uv build and twine check --strict pass, and the wheel is 1.1.1.
  • The Sphinx build passes with -W -n.

On free-threaded Python 3.14 a new thread starts in a copy of the
context that started it. A thread pool starts workers lazily and reuses
them, so each worker keeps the section it started under, and work
submitted later from another section is reported under the first one.
Regular builds report the same work at the top level.

The concurrency guide said pool work is always top-level (a comment the
docs test never asserted, and false on 3.14t) and that free-threaded
builds nest thread work with no extra work. It now explains the
difference and shows how to get the same report on every build: run
each call in copy_context().run to nest it, or Context().run to keep it
top-level. Both examples assert their results.

New tests pin how plain threads and reused pool workers nest on each
kind of build, and check both recipes. The library can't fix this
itself: it can't tell an intentionally inherited context from a stale
one. Behaviour is unchanged.

Bump __version__ to 1.1.1 and update the bug template's placeholder.
@seba2390
seba2390 merged commit 751950a into main Sep 30, 2026
19 checks passed
@seba2390
seba2390 deleted the docs/thread-context-by-build branch September 30, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant