Unified task offloading helpers for CPython 3.11+
Project description
unirun
unirun gives Python developers a standard-library-fluent interface for running
"everything-to-everything" workloads across CPython's evolving execution
models. The project's golden rule is simple: keep speaking in the vocabulary of
concurrent.futures, asyncio, and multiprocessing even as processes,
threads, sub-interpreters, and free-threaded builds converge. The helpers work
on CPython 3.11+, including the free-threaded builds slated for Python 3.14.
Features
- Golden rule baked in—every helper exposes familiar stdlib nouns (
Executor,Future,submit,map) so teams can adopt new runtimes without relearning terminology. - Capability detection that snapshots interpreter/GIL traits and suggests sane pool sizes.
- Managed executor factories (
thread_executor(),process_executor(),interpreter_executor()) that return realconcurrent.futures.Executorinstances with lifecycle handled for you. - Automatic scheduling via
get_executor(mode="auto", **hints)andrun(...)so call sites stay synchronous while the library picks an appropriate backend. - Async bridging helpers
to_thread/to_processthat wrap existingasynciopatterns instead of inventing new coroutine types. - Benchmark harness covering micro → macro scenarios without runtime
dependencies, plus an optional
unirun_benchCLI module for manual analysis.
Drop-In Parity, Optional Upgrades
Keep writing the stdlib code you already trust—unirun only steps in when you
want smoother ergonomics or smarter defaults.
| Stdlib pattern you keep using | Optional unirun assist |
What improves when you opt in |
|---|---|---|
Executor.submit(...).result() |
run(..., cpu_bound=True) |
Same synchronous call, plus capability-aware executor selection |
asyncio.to_thread(func, *args) |
to_thread(func, *args) |
Identical signature, auto-tuned pools on nogil builds |
executor.map(iterable) |
thread_executor().map(iterable) |
Familiar API, shared executor lifecycle handled for you |
ThreadPoolExecutor() context managers |
thread_executor() context manager |
Drop-in replacement with deterministic teardown and reset hooks |
Manual executor switching (if cpu: ...) |
get_executor(mode="auto", **hints) |
One call site; capabilities decide whether threads/processes win |
| Sub-interpreter experimentation | interpreter_executor() |
Presents the standard Executor surface with safe thread fallback |
Seamless asyncio.to_thread Upgrades
- The
to_threadhelper mirrorsasyncio.to_threadexactly, so call sites need no signature or import changes during migration. - Capability detection spots free-threaded (nogil) interpreters and routes work
through a tuned
thread_executor()that actually scales across cores, while falling back cleanly when nogil is unavailable. - Decision traces and logging hooks let teams verify why the scheduler chose a particular executor—critical when rolling out nogil builds incrementally.
Why This Still Matters on Python 3.14
- Executor management remains real work—
unirunsizes and names shared pools, registers shutdown hooks, and keeps lifecycle consistent across services so teams can focus on business logic. - Async bridges still need configuration—
to_threadand friends delegate to tuned executors automatically instead of requiring manual loop-level overrides in every coroutine. - Uniform APIs smooth mixed environments—even if production guarantees 3.14, local runs, tests, or downstream consumers may lag, so the same stdlib-shaped helpers behave correctly across interpreter versions without forks.
Life Without unirun
- Teams hand-roll capability checks (
sysconfig.get_config_var("Py_GIL_DISABLED")), scatter feature flags, and duplicate heuristics to guess pool sizes. - Each service implements its own executor lifecycle: global singletons,
atexithandlers, ad-hoc worker naming, and inconsistent shutdown semantics that often leak futures or swallowCancelledError. - Tests require bespoke fixtures to reset global executors and mock capability detection, fragmenting coverage across GIL and nogil environments.
- Documentation drifts away from stdlib language as wrappers like
run_in_threadsorbg_taskmultiply, raising the onboarding cost for new contributors.
# Typical DIY nogil helper without unirun
_FREE_THREAD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
_GLOBAL_EXECUTOR: ThreadPoolExecutor | None = None
def get_executor() -> ThreadPoolExecutor:
global _GLOBAL_EXECUTOR
if _GLOBAL_EXECUTOR is None:
max_workers = os.cpu_count() if _FREE_THREAD else (os.cpu_count() or 1) * 5
_GLOBAL_EXECUTOR = ThreadPoolExecutor(
max_workers=max_workers,
thread_name_prefix="app-nogil" if _FREE_THREAD else "app-gil",
)
atexit.register(_GLOBAL_EXECUTOR.shutdown)
return _GLOBAL_EXECUTOR
async def to_thread(func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
loop = asyncio.get_running_loop()
executor = get_executor() if _FREE_THREAD else None
return await loop.run_in_executor(executor, functools.partial(func, *args, **kwargs))
# With unirun the same coroutine stays readable
from unirun import to_thread
async def main() -> None:
await to_thread(func, *args, **kwargs) # signature matches asyncio.to_thread
Installation
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .
Hatchling powers packaging with dynamic (VCS-based) SemVer tags so publishing to PyPI only requires tagging:
hatch build
hatch publish
Quick Start
Automatic execution in one call
from unirun import run
from unirun.workloads import count_primes
result = run(count_primes, 250_000, cpu_bound=True)
print(result)
Manual control with a managed thread pool
from unirun import thread_executor
from unirun.workloads import simulate_blocking_io
durations = [0.01, 0.02, 0.03]
with thread_executor() as executor:
for value in executor.map(simulate_blocking_io, durations):
print(value)
Async bridging (drop-in parity with asyncio.to_thread)
import asyncio
from unirun import to_thread
from unirun.workloads import simulate_blocking_io
async def main() -> None:
await to_thread(simulate_blocking_io, 0.05)
asyncio.run(main())
Optional Benchmark CLI
The base library keeps runtime dependencies at zero. For manual benchmarking, invoke the optional CLI module without affecting core installs:
python -m unirun_bench --profile all --samples 5 --json
The CLI returns JSON (optionally annotated with capability snapshots) or prints
the formatted table generated by unirun.benchmarks.format_table.
Testing & Coverage
The test suite follows Kent Beck's TDD ethos—covering sync, async, CPU, IO, and process-path behaviors:
python -m unittest discover -s tests
Pytest modules are organized by feature: one test file per concurrency surface,
with matching _double.py companions to assert drop-in parity with the CPython
stdlib.
Mutation Testing
Mutation testing enforces that behavior-focused assertions fail when
capabilities or workloads break. The project relies on the
ensure-compatibility-with-python-3.14 fork of mutmut
so experiments stay green on the Python 3.14 alphas.
# Install developer dependencies, including the patched mutmut fork
uv sync --group dev
# Run the mutation suite with the built-in pytest runner
uv run mutmut run
# Inspect surviving mutants directly in the terminal (optional)
uv run mutmut results
The [tool.mutmut] block in pyproject.toml pins the mutation scope to
src/unirun while letting the CLI discover tests in tests/. This keeps the
suite aligned with the golden rule by mutating only the user-facing concurrency
helpers.
Design Notes
- No runtime third-party dependencies. Native accelerators remain optional and can be added via C extensions that participate in the free-threaded ABI.
- The golden rule applies to code and docs alike: describe behaviors with the
same nouns and verbs the Python stdlib already uses (
Executor,Future,submit,map,as_completed). - Capability detection relies solely on stdlib primitives so that behavior is stable across CPython releases and alternative builds (musl, manylinux, etc).
Release Automation
Trigger the Semantic Release with Girokmoji workflow from the Actions tab to generate release notes and version tags automatically.
- Launch the workflow manually and choose the semantic version segment to bump (
patch,minor, ormajor). - The pipeline installs dependencies with
uv, executesuv run pytest, and then invokesgirokmojito create a changelog (release.md). - Successful runs push the updated tag back to the repository, upload the changelog as an artifact, and publish a GitHub Release using the generated notes.
This workflow mirrors the reference pipeline in girokmoji so future tooling updates stay compatible with our release process.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unirun-0.1.0.tar.gz.
File metadata
- Download URL: unirun-0.1.0.tar.gz
- Upload date:
- Size: 32.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
482e5e3b4de326c4a9dcd0e6703876ac2ece19380988ed6a6d3b2855b92ceb46
|
|
| MD5 |
ea8abb60478d575942bd9df3928a014e
|
|
| BLAKE2b-256 |
2280d7f139532c4ceabae333e409de3eb77c290e3fee6c48eb28c08a1d1d39fc
|
Provenance
The following attestation bundles were made for unirun-0.1.0.tar.gz:
Publisher:
python-publish.yml on KMilhan/unirun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unirun-0.1.0.tar.gz -
Subject digest:
482e5e3b4de326c4a9dcd0e6703876ac2ece19380988ed6a6d3b2855b92ceb46 - Sigstore transparency entry: 583954457
- Sigstore integration time:
-
Permalink:
KMilhan/unirun@ca74e1e56ac61891ee033f96357ccccde35b1ecd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/KMilhan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ca74e1e56ac61891ee033f96357ccccde35b1ecd -
Trigger Event:
release
-
Statement type:
File details
Details for the file unirun-0.1.0-py3-none-any.whl.
File metadata
- Download URL: unirun-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92309d8f70f1fdb0973927467543ceef40498b43eddb03b9fb3e1a788ab4e696
|
|
| MD5 |
7237d84310f4673d18719f42c1171664
|
|
| BLAKE2b-256 |
4dee7590de017175ebbded9b84acfd500adb218d9b8fbbf7d8619e5f0c32e30d
|
Provenance
The following attestation bundles were made for unirun-0.1.0-py3-none-any.whl:
Publisher:
python-publish.yml on KMilhan/unirun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unirun-0.1.0-py3-none-any.whl -
Subject digest:
92309d8f70f1fdb0973927467543ceef40498b43eddb03b9fb3e1a788ab4e696 - Sigstore transparency entry: 583954458
- Sigstore integration time:
-
Permalink:
KMilhan/unirun@ca74e1e56ac61891ee033f96357ccccde35b1ecd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/KMilhan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ca74e1e56ac61891ee033f96357ccccde35b1ecd -
Trigger Event:
release
-
Statement type: