Skip to main content

latchkit

Lianes8 Certified

One-shot synchronization primitives for threads and asyncio: a count-down latch, a gate and a fire-once event, each with abort-with-reason, typed errors and timeouts that behave. Zero dependencies, fully typed, Python 3.10 and up.

Install

pip install latchkit

Quick start

Wait for a fixed number of workers to finish before moving on:

import threading

from latchkit import CountDownLatch

latch = CountDownLatch(3)


def worker(i: int) -> None:
    with latch.arrival():  # counts down when the block exits, even on error
        ...  # do the work


for i in range(3):
    threading.Thread(target=worker, args=(i,)).start()

latch.wait()  # blocks until all three have counted down

wait takes a timeout and returns whether the latch opened in time; wait_or_raise raises LatchTimeoutError (a TimeoutError) instead:

if not latch.wait(timeout=5.0):
    print("still waiting")
latch.wait_or_raise(timeout=5.0)

When something goes wrong, abort instead of leaving everyone blocked. Every current and future waiter raises BrokenLatchError with your reason:

from latchkit import BrokenLatchError

jobs = CountDownLatch(3)
jobs.abort(RuntimeError("database unreachable"))
try:
    jobs.wait()
except BrokenLatchError as error:
    print(error.reason)  # the RuntimeError, also chained as __cause__

A Gate is a manual switch that, once opened, stays open for every waiter. opening() guards a run-once initialisation: it opens on success and aborts on any failure (cancellation included), so waiters never hang on a crashed initialiser:

from latchkit import Gate

ready = Gate()

with ready.opening():
    ...  # load config, warm caches

ready.wait()  # returns at once, now and forever

A OneShotEvent fires exactly once and tells the caller who won the race:

from latchkit import OneShotEvent

claimed = OneShotEvent()
if claimed.set():  # True for the first caller only
    print("this thread won")

Every primitive has an asyncio twin with the same API. Signalling is thread-safe, so worker threads can release coroutines:

import asyncio

from latchkit import AsyncCountDownLatch


async def main() -> None:
    latch = AsyncCountDownLatch(4)
    loop = asyncio.get_running_loop()
    for _ in range(4):
        loop.run_in_executor(None, latch.count_down)
    await latch.wait(timeout=10)


asyncio.run(main())

The guide covers each primitive, abort and errors, argument rules, asyncio and thread bridging, timeouts, the exact memory-ordering guarantees and how the semantics compare with Java, .NET, C++ and Go. Changes are listed in the changelog.

API at a glance

Thread-based asyncio What it is
CountDownLatch(n) AsyncCountDownLatch(n) Opens after n signals: count_down, try_count_up, remaining, is_open, arrival()
Gate() AsyncGate() Opens once on demand: open, is_open, opening()
OneShotEvent() AsyncOneShotEvent() Fires once, names the winner: set, is_set

All six share wait(timeout), wait_or_raise(timeout), abort(reason), is_broken and abort_reason. Errors: LatchError, BrokenLatchError, LatchTimeoutError. SignalScope is the context manager that arrival() and opening() return.

Why

The standard library has threading.Event, threading.Barrier, threading.Condition and their asyncio cousins, and none of them is a count-down latch:

  • threading.Barrier is cyclic and symmetric: every participant must both arrive and wait, so one thread cannot wait while N unrelated threads each signal once. If you need a rendezvous, use it; latchkit does not duplicate it.
  • threading.Event and asyncio.Event are a single resettable boolean with no count, no notion of failure and no winner. A clear() can make a late waiter miss the signal, and a crashed producer leaves waiters blocked forever. asyncio's primitives are documented as not thread-safe, so a worker thread cannot set an asyncio.Event directly.
  • threading.Condition is the right building block, which is why people hand-roll a counter plus a condition and get the edge cases (counting past zero, waking everyone, failure, timeouts) subtly wrong. Even the standard waits have sharp edges: on the CPython 3.10 to 3.14 builds we tested (Windows) Event.wait(float("inf")) raises OverflowError, and Condition.wait_for(predicate, float("nan")) spins without ever sleeping. latchkit treats infinity as "forever" and refuses NaN.

The "wait for N things, then proceed, once" primitive is CountDownLatch in Java, CountdownEvent in .NET and std::latch in C++20. latchkit is that primitive for both threads and asyncio, checked against the Java and .NET implementations, plus the two one-shot shapes that come up next to it, and an abort path modelled on threading.Barrier.abort() so failures reach waiters instead of hanging them.

There is no CLI: these are in-process primitives, and a command line could not share one with anything.

License

MIT. See LICENSE.

Metadata

Release files for latchkit 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for latchkit 1.0.0
File Size Uploaded
latchkit-1.0.0.tar.gz 65.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for latchkit 1.0.0
File Interpreter ABI Platform
latchkit-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 85.1 kB

Release files / latchkit-1.0.0.tar.gz

Download URL latchkit-1.0.0.tar.gz
Size 65.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a0b7b9e1e18364ff957a5f611f17bf155bf9ed99b97eddc57a83f6f7f0e8c088
BLAKE2b-256 checksum
How to use checksums
00e928f574fa7ae83e90661aa76141a2fec7415434a9a089f9a59d72f656fe1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / latchkit-1.0.0-py3-none-any.whl

Download URL latchkit-1.0.0-py3-none-any.whl
Size 19.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e8630bfa00460045d95a3185be12b10d11235a95cf915988813abbb7f874c044
BLAKE2b-256 checksum
How to use checksums
7ace3476df9b3e8c565c9aa799a1b16b91173b443e62bf409f3c7263a0002f38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page