loopback-singleton
loopback-singleton is a lightweight Python package that gives multiple local processes access to a single shared object instance hosted in a background daemon on 127.0.0.1.
It is useful when you want one process-external object (cache, counter, coordinator, adapter, etc.) and you want all local workers to call into that object without standing up a full RPC system.
Current status (v0.2.4)
Current release: 0.2.4.
What works today
- Local singleton daemon auto-start on first use.
- Concurrent startup coordination with file locking to reduce duplicate daemons.
- Authenticated handshake (shared token in runtime dir) between client and daemon.
- Sequential method execution on the singleton object (single executor queue).
- Idle TTL auto-shutdown for daemon cleanup.
- Recovery from stale or corrupted runtime metadata.
- Cross-platform runtime location strategy (Windows + POSIX fallback behavior).
Installation
pip install loopback-singleton
For local development:
pip install -e .[dev]
Quickstart
Create a module with a factory target (class or callable):
# mypkg/services.py
class Counter:
def __init__(self):
self.value = 0
def inc(self) -> int:
self.value += 1
return self.value
def ping(self) -> str:
return "pong"
Use local_singleton from any process:
from loopback_singleton import local_singleton
svc = local_singleton(
name="my-counter",
factory="mypkg.services:Counter",
idle_ttl=2.0,
serializer="pickle",
)
with svc.proxy() as obj:
print(obj.ping())
print(obj.inc())
API overview
local_singleton(
name: str,
factory: str | callable | type,
*,
factory_args: tuple = (),
factory_kwargs: dict | None = None,
scope: str = "user",
idle_ttl: float = 2.0,
serializer: str = "pickle",
connect_timeout: float = 0.5,
start_timeout: float = 3.0,
)
name: singleton identity (shared runtime namespace).factory: import string ("module:callable_or_class") or module-level importable callable/class object.factory_args,factory_kwargs: constructor args used when creating singleton instance (factory_kwargs=Nonebehaves as{}).scope: currently only"user"is implemented.idle_ttl: daemon stops after this many seconds with zero active connections.serializer: currently only"pickle"is implemented.connect_timeout,start_timeout: socket/startup tuning.
svc.proxy() returns a dynamic proxy where method calls are forwarded to the daemon.
Additional lifecycle APIs are available on LocalSingletonService:
svc.ensure_started()
info = svc.ping()
svc.shutdown()
Discover running singletons
Use list_local_singletons() to inspect singleton runtime directories without changing daemon state.
from loopback_singleton import list_local_singletons
for item in list_local_singletons():
print(item.name, item.status, item.host, item.port, item.pid, item.serializer)
Notes:
- Read-only API: it does not take startup locks, spawn daemons, or clean stale files.
- Connection-related metadata comes from
runtime.binwhen available (host,port,pid,serializer, protocol fields). - Factory details are included by default (
factory_import,factory_args,factory_kwargs), and can be skipped withinclude_factory=False. - Auth secrets are not returned (only the auth file path is exposed in the result object).
- Directories that contain only
auth.bin(without runtime/factory metadata) are ignored by default. - Optional
probe=Trueperforms best-effort connectivity checks against already-running daemons and enrichesreachable/ping_infowithout auto-starting services.
How it works
- Client computes runtime paths for the singleton name.
- Client attempts connection using runtime metadata.
- If missing/failing, it takes a file lock, cleans stale metadata, and spawns daemon.
- Daemon binds ephemeral loopback TCP port, writes runtime metadata, and serves requests.
- Each
CALLrequest is executed sequentially against one in-memory object instance.
Lifecycle and robustness scenarios
Scenario 1 — Pass a class directly
from loopback_singleton import local_singleton
from mypkg.services import CounterService
svc = local_singleton(
"counter",
factory=CounterService,
factory_args=(10,),
factory_kwargs={"step": 2},
)
with svc.proxy() as p:
assert p.inc() == 12
assert p.inc() == 14
Scenario 2 — Pass a factory function directly
from loopback_singleton import local_singleton
from mypkg.factories import make_cache
svc = local_singleton(
"cache",
factory=make_cache,
factory_kwargs={"max_items": 1000, "ttl": 60},
)
with svc.proxy() as cache:
cache.put("k", "v")
assert cache.get("k") == "v"
Scenario 3 — Warm-up a configured daemon without proxy creation
svc = local_singleton("svc", factory=MyService, factory_args=(...), factory_kwargs={...})
svc.ensure_started()
info = svc.ping()
Scenario 4 — Non-importable factory gets a clear error
svc = local_singleton("x", factory=lambda: object())
# -> TypeError: Factory must be importable (module-level). Pass 'pkg.mod:callable' string instead.
Factory consistency across concurrent clients
For a given singleton name, the first daemon start wins. Subsequent clients must use the same normalized factory + args/kwargs. If they differ, the client fails fast with FactoryMismatchError.
Error model
Main exception classes exported by the package:
LoopbackSingletonError(base)DaemonConnectionErrorConnectionFailedErrorHandshakeError
ProtocolError(invalid or oversized transport frames/messages)FactoryMismatchError(running daemon factory config differs from requested config)RemoteError(remote traceback payload)
Security notes (important)
This MVP uses pickle for transport serialization. pickle is not safe for untrusted input and can execute arbitrary code.
Use this package only in trusted local environments for now.
Runtime files and cleanup
Runtime files are created under:
- Windows:
%LOCALAPPDATA%/loopback-singleton/<name>/ - Linux/macOS:
$XDG_RUNTIME_DIR/loopback-singleton/<name>/ - POSIX fallback:
~/.cache/loopback-singleton/<name>/
If startup repeatedly fails due to stale metadata, stop clients and remove the directory for that singleton name.
Known limitations (MVP)
- Callable/class factories must be importable at module level when passed as objects (lambdas/nested functions are rejected).
- No identity transparency for proxies (
isinstance(proxy, MyType)is not preserved). - No magic-method forwarding (
__len__, operators, iteration, etc.). - Only
scope="user"implemented. - Only
serializer="pickle"implemented (msgpackplaceholder exists but not implemented). - Transport is loopback TCP only.
Development
Run checks and tests:
ruff check .
pytest -q
Build package:
python -m build
Future work
Planned directions for post-MVP releases:
-
Safer serialization options
- Implement
msgpackserializer path and typed payload envelopes. - Add optional schema validation for RPC payloads.
- Implement
-
Richer proxy semantics
- Support selected dunder/magic methods.
- Improve error transport with structured remote exception metadata.
-
Lifecycle and observability
- Add daemon health/metrics endpoint(s) and lightweight tracing hooks.
- Expose explicit client APIs for graceful shutdown and restart policies.
-
Scope and deployment flexibility
- Add additional scope modes beyond per-user.
- Evaluate optional Unix domain socket transport on POSIX.
-
Robustness and compatibility
- Protocol version negotiation for rolling upgrades.
- Expanded stress/regression suite for high-concurrency scenarios.
-
Security hardening
- Optional mutual-auth improvements and stricter runtime file hardening.
- Guidance and tooling for locked-down local deployments.
Contributions and issue reports are welcome at:
Metadata
Release files for loopback-singleton 0.2.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| loopback_singleton-0.2.4.tar.gz | 33.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| loopback_singleton-0.2.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.7 kB
Release files / loopback_singleton-0.2.4.tar.gz
| Download URL | loopback_singleton-0.2.4.tar.gz |
|---|---|
| Size | 33.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89bcb4f46992a1e779668726fd44ac3ed62112920fab8250bc51e15a32a78017
|
|
BLAKE2b-256 checksum How to use checksums |
1029804618cd872b06c61fc9e33e029a3486f2df8bf9b60998e09d291bf472e9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 15, 2026.
Transparency logRelease files / loopback_singleton-0.2.4-py3-none-any.whl
| Download URL | loopback_singleton-0.2.4-py3-none-any.whl |
|---|---|
| Size | 20.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12c6b784edc1ca584a97ff71073664c519a993d59d7adedd7955940fe8d4b791
|
|
BLAKE2b-256 checksum How to use checksums |
efe0065ce1a21cd6e2543a02be208dafaa378f33b970cf5389b29c27735cc025
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 15, 2026.
Transparency log