You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
stdlib: threading.Thread edges diverge — join() before start() escapes as System.Threading.ThreadStateException, a default name is None #2261
threading.Thread's surface diverges from CPython at its edges. Found while making threading.Timer a Thread (#2251, 474894f). Contract: every Thread operation gives CPython's value, or raises CPython's exception type, which a Sharpy except clause can catch.
Cells
cell
program
observed @ sha
expected (python3 3.12)
lane
reach
found by
join before start
t = threading.Thread(target=f) · try: t.join() · except RuntimeError: print("caught")
unhandled System.Threading.ThreadStateException: Thread has not been started. — except RuntimeError does not catch it @ b44111020
RuntimeError: cannot join thread before it is started (caught)
Batching 2026-10-06 @ 5942b6d — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.0, chores (order 0; no plan) — in TWO parts: part 1 now, part 2 directly after P54 (#2250). This issue is the tracker and stays open across both. Relabelled reach:program (an ordinary t.is_alive() is refused). Rulings R-FC, R-FD, R-FE, R-FF (owner, 2026-10-06).
Contract: every Thread operation gives CPython's value, or raises CPython's exception type, which a Sharpy except clause can catch. Lane / reach: runtime-throw · program. Matrix: operation {start, join, join(timeout), daemon get/set, name get/set, is_alive, ident} × state {initial, alive, finished, current thread} × host {Thread, Timer, subclass, the current_thread() / main_thread() wrapper}, plus module function {current_thread, main_thread, enumerate, active_count} × caller {main thread, worker}. Expectations are python3 3.12 literals in one table-driven theory.
RuntimeError: cannot set daemon status of active thread
1
new: join the current thread
threading.current_thread().join()
hangs
RuntimeError: cannot join current thread
1
start twice
t.start() after a join
RuntimeError (caught)
same
matches
new:is_alive kind
t.is_alive()
SPY0230 "Expression of type 'bool' is not callable" (t.is_alive prints False; Event.is_set() is a method)
a method
1
new:args= / kwargs=
Thread(target=g, args=(3,))
SPY0234 (the target=lambda: g(3) twin runs and prints 3)
runs
1 — ledger row, R-FC
new:ident before start
print(t.ident)
4
None
1 — ledger row, R-FE
default name
Thread(target=f).name; a subclass; current_thread().name; main_thread().name
None ×4
Thread-1 (f); Thread-N; MainThread; MainThread
2 — R-FD
new:main_thread() from a worker
main_thread().ident == current_thread().ident
True
False
2
new:enumerate() from a worker
len(threading.enumerate())
1
2
2
new:active_count() after the join
threading.active_count()
9
1
2
A subclass's run needs @override (the #2138 ruling) — not a cell.
Part 1 (now; adds no state): the three exception-type cells; is_alive becomes a method; the two ledger rows. Part 2 (directly after P54; adds process-wide state): the default name and MainThread; main_thread(), enumerate(), active_count(). Timer inherits every cell.
Ruling R-FC — "a: deviation row with the lambda steer".Thread(target=…, args=…, kwargs=…) is the row no-thread-args-kwargs; its executed example is threading.Thread(target=lambda: g(3)). Rejected: (b) typed generic factories for arity 1–N — C# constructors cannot be generic, so they would be module-level factories beside the type and a subclass would not get them; it stacks later (additive); (c) untyped args: tuple invoked dynamically — runtime type checking.
Ruling R-FD — "a: Thread-N now, MainThread for the main thread, the (target) suffix as a cell on #2105".name becomes a non-null str. Rejected: (b) the full Thread-N (f) in one step after P11g (name stays None until then); (c) a deviation row with name left None (a str? narrowed at every use). The printed value changes twice for target threads.
Ruling R-FE — "a: ledger row".ident before start() is the row thread-ident-before-start; the id's value is an implementation detail. Rejected: (b) int | None (narrowing at every use; C# int → int?).
Ruling R-FF — "b: split at the state boundary". Rejected: (a) one chore now (adds process-wide state before P54's harness exists); (c) an own group after P54 with P38's roster extended to the threading family (a verify round for a day of work).
Probe note: the join-current-thread cell deadlocks the compiled program; run it under a timeout.
Part 1 landed (R-FF), in commit 7ab7a97 on dev. The issue stays open for part 2, which comes directly after P54 (R-FD: default names Thread-N / MainThread, main_thread(), enumerate(), active_count()).
What changed.Thread keeps a started flag and a cached daemon flag, and checks those instead of the .NET thread's state. The flag is set atomically by start(), and the current_thread() wrapper is created started. So start() twice, join() before start(), join() on the current thread, and setting daemon once started each raise RuntimeError with python's message, and a Sharpy except RuntimeError catches it. is_alive() is now a method, like Event.is_set().
Guard.ThreadStateMatrixTests (Stdlib.Tests) runs one Sharpy program and compares its 81 output lines with the output of the same program written in python, run by python3 3.12.13. The matrix:
operation {start(), join(), join(0.01), daemon, daemon = True, is_alive(), name = "x" read back}
× state {initial, alive, finished}
× host {Thread, Timer, a subclass}
plus current_thread() on the main thread and on a worker, and a worker acting on its own Thread.
Alive threads are held by an Event, so no cell depends on timing.
Mutations. All five go red: dropping each of the three state checks, reverting the daemon cache, and reverting is_alive to a property. Details are in the commit body. The gate result is below.
New cell found by the matrix. Reading t.daemon on a finished thread also escaped ThreadStateException, because .NET's IsBackground getter throws once a thread is dead. Fixed here.
Ledger rows. Every example in both rows was executed on both sides at this tree.
no-thread-args-kwargs (R-FC). Correction to the batching note: kwargs= is refused with SPY0234 alone. The SPY0227 in the earlier probe came from its mixed int/str dict literal, not from kwargs=.
thread-ident-before-start (R-FE).
Out of scope, filed by class: #2263. A threading timeout or interval is converted to a .NET wait with (int)(timeout * 1000), so the result is not python's verdict:
a negative join / Event.wait / Barrier.wait / Timer value escapes ArgumentOutOfRangeException;
a negative Lock / Semaphore timeout hangs;
1e10 hangs or saturates instead of raising OverflowError.
This was found on the prober budget's timeout-value axis.
Gate: whole solution 57951/0/4 @ 5942b6d + both slices (measured); spy-stdlib, spy-test, stdlib-doc staleness, whitespace and allowlist issue-state clean. Pushed dev + mainline @ 90f7ff9.
→ observed: Unhandled exception. Sharpy.ValueError: boom, exit 134, and the last line never prints. Expected (python3 3.12.13, the same program with target=boom and a main() call): Exception in thread Thread-1 (boom): plus the traceback on stderr (threading.excepthook), then main continues after thread exception, exit 0. Lane silent-wrong, reach program. Operation axis: target raises × host {Thread, Timer, subclass run}.
A member of the cell after #2263: threading.Timer(1e10, f).start() now raises OverflowError on the timer thread as python does, and that ends the process. Python prints Exception in thread Thread-1: / OverflowError: …, never calls f, and the main thread continues. Before #2263 the timer waited about 24.8 days. The #2263 guard leaves out Timer(1e10) and cites this cell.
Landed (chores band 2026-10-22) on dev @ 4d22212, PR #2346 to mainline. Whole-solution gate 81669/0/7 @ 4d22212 (measured), against 81093/0/7 @ be4664e.
Direction, from a worker (python3 3.12.13 in brackets):
main ident == current ident: True → False [False].
len(enumerate()): 1 → 2 [2].
active_count() after the join: 9 → 1 [1].
Guards:
ThreadStateMatrixTests is extended to 126 lines checked against python.
The P54 roster gains ThreadRegistry._counter and ThreadRegistry._active.
Seven mutations, each red.
Stays open. The 2026-10-06 cell (an exception raised in a thread's target ends the whole process; Timer(1e10) is a member) was not in either part. StdlibTimeoutMatrixTests cites it. This issue is now its tracker until /batch-issues places it.
Batching 2026-10-22 @ 4d22212 — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.0, chores — call-28 band: #2261 part 3 (order 0, one commit, no plan; rides P21e's gate; runs under P54's harness). Ruling R-IG (owner, 2026-10-22): D7 a — a Stdlib Threading/ chore; this issue CLOSES on the python-twin table.
Cell (the 2026-10-06 comment):t = threading.Thread(target=boom); t.start(); t.join(); print("main continues") with boom raising ValueError("boom") → observed Unhandled exception. Sharpy.ValueError: boom at Sharpy.Thread.<>c__DisplayClass5_0.<.ctor>b__0(), exit 134, the last line never printed @ be4664e; expected (python3 3.12) Exception in thread Thread-1 (boom): + traceback on stderr via threading.excepthook, the last line printed, exit 0. Lane runtime-throw, reach program. Sibling cells @ be4664e: a daemon thread's child reports daemon False (python True — daemon=None inherits the creator's flag; Thread.cs:62 sets IsBackground from a ctor parameter defaulting to false); Thread.cs:71,79 expose settable Name/Daemon while docs/stdlib/threading.md's Properties table lists only ident (docs generator or doc-comment gap — the chore finds which); the threading_stub.spy docstring staleness is the band agent's note (unverified).
R-IG (a): the target — and a subclass's run — executes under a wrapper that writes Exception in thread <name>: plus the python-spelled exception type and message to stderr and returns, so the process continues; matrix target raises × host {Thread, Timer, subclass run} × exception kind {Sharpy exception, CLR exception}; exit code and "main continues" are COMPARED (control flow), the stderr text beyond the type name is deviation (R-CI); ThreadStateMatrixTests extended; the Timer(1e10) cell joins and the StdlibTimeoutMatrixTests cite drains in the same commit (check_allowlist_issue_state.sh reddens if this issue closes first); the daemon-inheritance row, the docs table and the stub docstring ride; a hanging cell runs in its own process group under a timeout. Rejected: (b) a P33 cell (P33's contract is the exception TYPE hierarchy and except*; this is where an unhandled exception goes, not what type it is; row 8 behind the keyword codemods); (c) an own group (one seam, three hosts). A program cannot depend on a crash — two-way in practice. R-FC/R-FD/R-FE/R-FF stand.
Daemon inheritance:daemon is bool? = None and inherits the creator's flag. A child of a daemon thread reports True here and in python3.
The Timer(1e10) cell: it joins the matrix (OverflowError in the timer thread, reported, main continues). StdlibTimeoutMatrixTests no longer cites this issue.
Docs: the Thread Properties table now lists name and daemon. The cause was the docs generator, which skipped every property whose accessor block opens on the next line; fixing it added 133 lines over 19 pages. The newly rendered re.Pattern.regex (.NET Regex) is posted on #2164.
Stub:threading_stub.spy's enumerate() docstring and body now delegate to ThreadingModule.enumerate().
Guard:ThreadStateMatrixTests.AnExceptionInAThread_IsReportedOnStderr_AndTheProcessContinues compares the exit code, the main lines and the report lines against python3 3.12.13.
Mutation records:
catch removed: red, exit 134.
daemon ?? false: red.
Generator fix reverted: the new pytest goes red.
Gate follow-up @ c771866: inheriting the flag reads current_thread(), which on a thread the threading module did not start makes a Dummy-N that draws a counter number. python3 3.12 does the same (Thread() on a _thread thread: ['MainThread', 'Dummy-2'], next name Thread-3). The module-global concurrency harness's own threads are such threads, so 15 of its cells went red. Its constructions now pass daemon explicitly, as python's Thread(daemon=False) skips current_thread(). Expected values are unchanged.
Closing: every part is done (part 1 @ 7ab7a97, part 2 @ bdd442f, part 3 here).
Activity
antonsynd commentedon Oct 6, 2026
Batching 2026-10-06 @ 5942b6d — charter
.claude/plans/remediation-round-2026-09-20-batching.md§3.0, chores (order 0; no plan) — in TWO parts: part 1 now, part 2 directly after P54 (#2250). This issue is the tracker and stays open across both. Relabelledreach:program(an ordinaryt.is_alive()is refused). Rulings R-FC, R-FD, R-FE, R-FF (owner, 2026-10-06).Contract: every
Threadoperation gives CPython's value, or raises CPython's exception type, which a Sharpyexceptclause can catch. Lane / reach: runtime-throw · program. Matrix: operation {start, join, join(timeout), daemon get/set, name get/set, is_alive, ident} × state {initial, alive, finished, current thread} × host {Thread,Timer, subclass, thecurrent_thread()/main_thread()wrapper}, plus module function {current_thread,main_thread,enumerate,active_count} × caller {main thread, worker}. Expectations are python3 3.12 literals in one table-driven theory.Cells measured @ 5942b6d (Debug binary built 2026-10-06 01:26; python3 3.12.13):
t.join()insidetry … except RuntimeErrorSystem.Threading.ThreadStateExceptionRuntimeError(caught)t.start(); t.join(); t.daemon = TrueThreadStateException: Thread is deadRuntimeError: cannot set daemon status of active threadthreading.current_thread().join()RuntimeError: cannot join current threadt.start()after a joinRuntimeError(caught)is_alivekindt.is_alive()t.is_aliveprintsFalse;Event.is_set()is a method)args=/kwargs=Thread(target=g, args=(3,))target=lambda: g(3)twin runs and prints3)identbefore startprint(t.ident)4NoneThread(target=f).name; a subclass;current_thread().name;main_thread().nameNone×4Thread-1 (f);Thread-N;MainThread;MainThreadmain_thread()from a workermain_thread().ident == current_thread().identTrueFalseenumerate()from a workerlen(threading.enumerate())12active_count()after the jointhreading.active_count()91A subclass's
runneeds@override(the #2138 ruling) — not a cell.Part 1 (now; adds no state): the three exception-type cells;
is_alivebecomes a method; the two ledger rows. Part 2 (directly after P54; adds process-wide state): the default name andMainThread;main_thread(),enumerate(),active_count().Timerinherits every cell.Ruling R-FC — "a: deviation row with the lambda steer".
Thread(target=…, args=…, kwargs=…)is the rowno-thread-args-kwargs; its executed example isthreading.Thread(target=lambda: g(3)). Rejected: (b) typed generic factories for arity 1–N — C# constructors cannot be generic, so they would be module-level factories beside the type and a subclass would not get them; it stacks later (additive); (c) untypedargs: tupleinvoked dynamically — runtime type checking.Ruling R-FD — "a:
Thread-Nnow,MainThreadfor the main thread, the(target)suffix as a cell on #2105".namebecomes a non-nullstr. Rejected: (b) the fullThread-N (f)in one step after P11g (namestaysNoneuntil then); (c) a deviation row withnameleftNone(astr?narrowed at every use). The printed value changes twice for target threads.Ruling R-FE — "a: ledger row".
identbeforestart()is the rowthread-ident-before-start; the id's value is an implementation detail. Rejected: (b)int | None(narrowing at every use; C#int→int?).Ruling R-FF — "b: split at the state boundary". Rejected: (a) one chore now (adds process-wide state before P54's harness exists); (c) an own group after P54 with P38's roster extended to the threading family (a verify round for a day of work).
Probe note: the join-current-thread cell deadlocks the compiled program; run it under a timeout.
stdlib(threading): Thread state misuse raises python's RuntimeError, …
antonsynd commentedon Oct 6, 2026
Part 1 landed (R-FF), in commit 7ab7a97 on
dev. The issue stays open for part 2, which comes directly after P54 (R-FD: default namesThread-N/MainThread,main_thread(),enumerate(),active_count()).What changed.
Threadkeeps a started flag and a cacheddaemonflag, and checks those instead of the .NET thread's state. The flag is set atomically bystart(), and thecurrent_thread()wrapper is created started. Sostart()twice,join()beforestart(),join()on the current thread, and settingdaemononce started each raiseRuntimeErrorwith python's message, and a Sharpyexcept RuntimeErrorcatches it.is_alive()is now a method, likeEvent.is_set().Guard.
ThreadStateMatrixTests(Stdlib.Tests) runs one Sharpy program and compares its 81 output lines with the output of the same program written in python, run by python3 3.12.13. The matrix:start(),join(),join(0.01),daemon,daemon = True,is_alive(),name = "x"read back}Thread,Timer, a subclass}current_thread()on the main thread and on a worker, and a worker acting on its ownThread.Alive threads are held by an
Event, so no cell depends on timing.Mutations. All five go red: dropping each of the three state checks, reverting the
daemoncache, and revertingis_aliveto a property. Details are in the commit body. The gate result is below.New cell found by the matrix. Reading
t.daemonon a finished thread also escapedThreadStateException, because .NET'sIsBackgroundgetter throws once a thread is dead. Fixed here.Ledger rows. Every example in both rows was executed on both sides at this tree.
no-thread-args-kwargs(R-FC). Correction to the batching note:kwargs=is refused with SPY0234 alone. The SPY0227 in the earlier probe came from its mixedint/strdict literal, not fromkwargs=.thread-ident-before-start(R-FE).Out of scope, filed by class: #2263. A threading timeout or interval is converted to a .NET wait with
(int)(timeout * 1000), so the result is not python's verdict:join/Event.wait/Barrier.wait/Timervalue escapesArgumentOutOfRangeException;Lock/Semaphoretimeout hangs;1e10hangs or saturates instead of raisingOverflowError.This was found on the prober budget's timeout-value axis.
Gate: whole solution 57951/0/4 @ 5942b6d + both slices (measured); spy-stdlib, spy-test, stdlib-doc staleness, whitespace and allowlist issue-state clean. Pushed dev + mainline @ 90f7ff9.
antonsynd commentedon Oct 6, 2026
Cell (found by the #2263 chore, 2026-10-06 @ 90f7ff9): an exception raised in a thread's target ends the whole process.
→ observed:
Unhandled exception. Sharpy.ValueError: boom, exit 134, and the last line never prints. Expected (python3 3.12.13, the same program withtarget=boomand amain()call):Exception in thread Thread-1 (boom):plus the traceback on stderr (threading.excepthook), thenmain continues after thread exception, exit 0. Lane silent-wrong, reach program. Operation axis: target raises × host {Thread,Timer, subclassrun}.A member of the cell after #2263:
threading.Timer(1e10, f).start()now raisesOverflowErroron the timer thread as python does, and that ends the process. Python printsException in thread Thread-1:/OverflowError: …, never callsf, and the main thread continues. Before #2263 the timer waited about 24.8 days. The #2263 guard leaves outTimer(1e10)and cites this cell.stdlib: a timeout in seconds gives python's verdict in threading, sub…
fix(threading): a Thread is named Thread-N or MainThread, and main_th…
antonsynd commentedon Oct 22, 2026
Landed (chores band 2026-10-22) on
dev@ 4d22212, PR #2346 to mainline. Whole-solution gate 81669/0/7 @ 4d22212 (measured), against 81093/0/7 @ be4664e.Part 2, commit bdd442f (R-FD a, R-FF b).
Threading/ThreadRegistrymodels CPython 3.12's_counter,_activeand_main_thread.nameis a non-nullstr:Thread-N, orMainThreadfor the main thread.main_thread(),enumerate()andactive_count()read the registry.(target)suffix stays a core: a function object prints its CLR delegate type (System.Func`2[...]); type(f).__name__ is 'Func' #2105 cell, per R-FD.Direction, from a worker (python3 3.12.13 in brackets):
main ident == current ident:True→False[False].len(enumerate()):1→2[2].active_count()after the join:9→1[1].Guards:
ThreadStateMatrixTestsis extended to 126 lines checked against python.ThreadRegistry._counterandThreadRegistry._active.Stays open. The 2026-10-06 cell (an exception raised in a thread's target ends the whole process;
Timer(1e10)is a member) was not in either part.StdlibTimeoutMatrixTestscites it. This issue is now its tracker until/batch-issuesplaces it.Merge pull request #2346 from antonsynd/dev
antonsynd commentedon Oct 22, 2026
Batching 2026-10-22 @ 4d22212 — charter
.claude/plans/remediation-round-2026-09-20-batching.md§3.0, chores — call-28 band: #2261 part 3 (order 0, one commit, no plan; rides P21e's gate; runs under P54's harness). Ruling R-IG (owner, 2026-10-22): D7 a — a StdlibThreading/chore; this issue CLOSES on the python-twin table.Cell (the 2026-10-06 comment):
t = threading.Thread(target=boom); t.start(); t.join(); print("main continues")withboomraisingValueError("boom")→ observedUnhandled exception. Sharpy.ValueError: boomatSharpy.Thread.<>c__DisplayClass5_0.<.ctor>b__0(), exit 134, the last line never printed @ be4664e; expected (python3 3.12)Exception in thread Thread-1 (boom):+ traceback on stderr viathreading.excepthook, the last line printed, exit 0. Lane runtime-throw, reach program. Sibling cells @ be4664e: a daemon thread's child reportsdaemonFalse (python True —daemon=Noneinherits the creator's flag;Thread.cs:62setsIsBackgroundfrom a ctor parameter defaulting to false);Thread.cs:71,79expose settableName/Daemonwhiledocs/stdlib/threading.md's Properties table lists onlyident(docs generator or doc-comment gap — the chore finds which); thethreading_stub.spydocstring staleness is the band agent's note (unverified).R-IG (a): the target — and a subclass's
run— executes under a wrapper that writesException in thread <name>:plus the python-spelled exception type and message to stderr and returns, so the process continues; matrix target raises × host {Thread, Timer, subclassrun} × exception kind {Sharpy exception, CLR exception}; exit code and "main continues" are COMPARED (control flow), the stderr text beyond the type name is deviation (R-CI);ThreadStateMatrixTestsextended; theTimer(1e10)cell joins and theStdlibTimeoutMatrixTestscite drains in the same commit (check_allowlist_issue_state.shreddens if this issue closes first); the daemon-inheritance row, the docs table and the stub docstring ride; a hanging cell runs in its own process group under a timeout. Rejected: (b) a P33 cell (P33's contract is the exception TYPE hierarchy andexcept*; this is where an unhandled exception goes, not what type it is; row 8 behind the keyword codemods); (c) an own group (one seam, three hosts). A program cannot depend on a crash — two-way in practice. R-FC/R-FD/R-FE/R-FF stand.fix(stdlib): an exception in a thread's target is reported on stderr …
test(stdlib): the module-global concurrency harness constructs every …
Merge dev into mainline: call-28 band slice 2 (refs #2057 #2143 #2342 #…
antonsynd commentedon Oct 22, 2026
Landed: part 3 @ 5782aa8 (R-IG).
An exception escaping a thread's target,
run()or Timer function is reported on stderr and the process continues:Exception in thread NAME:+Type: message, CPython's first and last lines. The traceback frames are R-CI deviation text.SystemExitis silent.Unhandled exception. Sharpy.ValueError: boom, exit 134.Daemon inheritance:
daemonisbool? = Noneand inherits the creator's flag. A child of a daemon thread reports True here and in python3.The
Timer(1e10)cell: it joins the matrix (OverflowErrorin the timer thread, reported, main continues).StdlibTimeoutMatrixTestsno longer cites this issue.Docs: the Thread Properties table now lists
nameanddaemon. The cause was the docs generator, which skipped every property whose accessor block opens on the next line; fixing it added 133 lines over 19 pages. The newly renderedre.Pattern.regex(.NETRegex) is posted on #2164.Stub:
threading_stub.spy'senumerate()docstring and body now delegate toThreadingModule.enumerate().Guard:
ThreadStateMatrixTests.AnExceptionInAThread_IsReportedOnStderr_AndTheProcessContinuescompares the exit code, the main lines and the report lines against python3 3.12.13.Mutation records:
catchremoved: red, exit 134.daemon ?? false: red.Gate follow-up @ c771866: inheriting the flag reads
current_thread(), which on a thread the threading module did not start makes aDummy-Nthat draws a counter number. python3 3.12 does the same (Thread()on a_threadthread:['MainThread', 'Dummy-2'], next nameThread-3). The module-global concurrency harness's own threads are such threads, so 15 of its cells went red. Its constructions now passdaemonexplicitly, as python'sThread(daemon=False)skipscurrent_thread(). Expected values are unchanged.Closing: every part is done (part 1 @ 7ab7a97, part 2 @ bdd442f, part 3 here).