smallOS is a lightweight cooperative runtime for priority-oriented task
management.
It is designed around three ideas:
- write tasks with modern
async/awaitsyntax - keep scheduling policy owned by
smallOS, notasyncio - stay portable enough to run on desktop Python today and MicroPython boards later
The project is currently experimental but usable. The runtime core supports:
- priority-based cooperative scheduling
- task spawning,
join, andjoin_all - signal-based wakeups
- time-based sleeping
- readiness-based socket/I/O waiting
- generic TCP/TLS kernel hooks for higher-level protocols
- dependency-free thread and asyncio execution adapters for user libraries
- smallOS-native HTTP, Redis, MQTT, SSE, and WebSocket helper clients
Python's asyncio gives great syntax, but it also brings its own scheduler and
event-loop policy. smallOS keeps the syntax while swapping in a custom
runtime, so tasks can be scheduled with project-specific priority rules and a
smaller portability surface.
That makes it a good fit for:
- robotics or device-control projects
- embedded experiments on MicroPython boards
- custom runtimes where task priority matters
- learning how coroutine scheduling works under the hood
- SmallPackage/SmallOS.py: cooperative scheduler
- SmallPackage/SmallTask.py: task lifecycle, coroutine stepping, join bookkeeping
- SmallPackage/Kernel.py: desktop and MicroPython kernel abstractions
- SmallPackage/SmallIO.py: buffered app/shell output routing and terminal-mode helpers
- SmallPackage/clients: protocol client package for cooperative network integrations
- SmallPackage/adapters: dependency-free escape hatches for blocking and asyncio-owned user code
- SmallPackage/clients/README.md: detailed client-specific guide and API notes
- SmallPackage/clients/SmallHTTP.py: dependency-free HTTP and SSE clients for smallOS tasks
- SmallPackage/clients/SmallStream.py: cooperative socket stream helper for protocol clients
- SmallPackage/clients/SmallRedis.py: dependency-free Redis client for smallOS tasks
- SmallPackage/clients/SmallMQTT.py: dependency-free MQTT client for smallOS tasks
- SmallPackage/clients/SmallWebSocket.py: dependency-free WebSocket client for bidirectional messaging
- SmallPackage/SmallConfig.py: runtime configuration loader/container
- smallos.config.json: repo-level runtime defaults
- SmallPackage/shells.py: command shell helpers for runtime inspection and demos
- demos: desktop and board-specific demo entry points
- tests: unit tests for scheduler, kernel, config, and supporting structures
Desktop development:
python3.10 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"smallOS supports CPython 3.10 and newer. Python 3.6 through 3.9 are no longer supported. MicroPython compatibility is maintained separately because its language and standard-library support do not map directly to a CPython release number; checker-only imports are kept off embedded runtime paths.
Run the test suite:
python3 -m unittest discover -s tests -vRun the tests with the same branch-coverage gate used by CI:
coverage run -m unittest discover -s tests -v
coverage reportRun the static type checker:
pyrightBuild the wheel and source distribution:
python -m buildThe GitHub Actions pipeline runs four gates: Pyright, unit tests across Python 3.10–3.13, branch coverage with a 60% floor, and distribution verification. Packaging runs only after the earlier gates pass, installs the built wheel, and smoke-tests it outside the source checkout.
The package ships a py.typed marker. Type coverage is being tightened by
subsystem: configuration, awaitables, task lifecycle, scheduling, signals,
platform kernels, and core utilities form the current checked boundary, while
protocol clients, shells, and demos remain on the incremental typing backlog.
Minimal desktop runtime:
from SmallPackage.Kernel import Unix
from SmallPackage.SmallConfig import SmallOSConfig
from SmallPackage.SmallOS import SmallOS
from SmallPackage.SmallTask import SmallTask
async def hello(task):
task.OS.print("hello from smallOS\n")
await task.sleep(0.1)
return "done"
config = SmallOSConfig.from_json_file("smallos.config.json")
runtime = SmallOS(config=config).setKernel(Unix())
runtime.setErrorHandler(
lambda event: print(
"[smallOS] task failure in {} (PID {}): {}".format(
event["task_name"] or "unnamed task",
event["task_id"],
event["exception_repr"],
)
)
)
runtime.fork([SmallTask(2, hello, name="hello")])
runtime.startOS()smallOS now supports a runtime-level error observer through
runtime.setErrorHandler(handler, include_cancelled=False).
Use it when you want:
- readable debug output for uncaught task failures
- lightweight cleanup or bookkeeping at the runtime boundary
- a single place to surface task errors without crashing the scheduler
The handler is synchronous and receives a failure-event dictionary after the task has been finalized. Current event fields include:
task_idtask_nameparent_idexceptionexception_typeexception_repris_cancelledblocked_reasonwaiting_signalio_wait_modejoin_target_idjoin_pending_idsadapter_nameadapter_job_idtraceback_text
By default, TaskCancelledError does not trigger the handler. Pass
include_cancelled=True if you want cancellation events too.
Example:
from SmallPackage.Kernel import Unix
from SmallPackage.SmallOS import SmallOS
def log_runtime_error(event):
print(
"[smallOS] task failure in {} (PID {}): {}".format(
event["task_name"] or "unnamed task",
event["task_id"],
event["exception_repr"],
)
)
if event["traceback_text"]:
print(event["traceback_text"], end="")
runtime = SmallOS().setKernel(Unix())
runtime.setErrorHandler(log_runtime_error)Closed or invalid file descriptors used in wait_readable(...) or
wait_writable(...) no longer crash the whole scheduler through the platform
poll/select layer.
Instead:
- the kernel validates the watched object before polling
- the waiting task receives a normal exception such as
ValueError - the runtime finalizes that task cleanly
- your runtime error handler can log or clean up the failure gracefully
If you do not install an error handler, the task still fails cleanly and the
runtime keeps its internal state consistent, but adding setErrorHandler(...)
is the recommended way to make these failures visible in applications.
The runtime now uses a first-class config object backed by smallos.config.json.
Current config fields:
task_capacity: maximum tracked tasks / PID slotspriority_levels: number of ready-queue categoriesio_buffer_length: buffered app output length when the terminal view is hiddeneternal_watchers: keep the runtime alive when only watcher tasks remainclient_defaults: shared defaults for cooperative clients and streams
Example:
{
"task_capacity": 1024,
"priority_levels": 10,
"io_buffer_length": 1024,
"eternal_watchers": false,
"client_defaults": {
"stream": {
"max_buffer_size": 16777216
},
"http": {
"max_response_size": 16777216
},
"redis": {
"max_response_size": 16777216,
"max_nesting_depth": 32
},
"mqtt": {
"keepalive": 60,
"max_packet_size": 262144,
"max_queued_messages": 1024
}
}
}The config loader also accepts the aliases oslist_length and
num_categories so older notes and experiments can map cleanly onto the
current runtime.
Client constructors still accept explicit overrides, but when you create them
inside a task they now inherit these defaults from task.OS.config unless you
pass a value directly.
Desktop kernel:
Unix
MicroPython kernels:
MicroPythonKernelESP32PicoW/RaspberryPiPicoWESP8266compatibility profile
You can either pick a board profile explicitly or let the runtime choose a built-in profile from the firmware machine string:
from SmallPackage.Kernel import ESP32, PicoW, build_micropython_kernel
kernel = ESP32(hostname="smallos-esp32")
kernel = PicoW(country="US", hostname="smallos-pico")
kernel = build_micropython_kernel()The new demos live in demos:
- demos/unix_demo.py: desktop scheduler demo
- demos/esp32_demo.py: ESP32-oriented startup and optional Wi-Fi bring-up example
- demos/pico_w_demo.py: Pico W oriented startup and Wi-Fi configuration example
- demos/micropython_autodetect_demo.py: automatic MicroPython kernel selection
- demos/runtime_demo.py: migrated home for the original root-level runtime showcase
- demos/shell_demo.py: scripted shell session running alongside other cooperative tasks
- demos/redis_demo.py: Redis example built on the native cooperative client
- demos/http_demo.py: HTTP example built on the native cooperative client
- demos/web_app_demo.py: cooperative single-thread web app demo with HTTP routes, live browser UI, and shell-driven server shutdown
- demos/mqtt_demo.py: MQTT example built on the native cooperative client
- demos/adapters_demo.py: thread and asyncio escape hatches running beside a regular SmallOS task
- demos/adapters_sqlite_demo.py:
a user-owned
sqlite3connection kept on one thread-adapter worker - demos/adapters_asyncio_demo.py: a persistent asyncio queue and background task reused across adapter calls
All of the shared demo entry points now install a default runtime error handler through demos/common.py. That means network failures, invalid I/O wait objects, and other uncaught task exceptions are reported as readable task-failure diagnostics instead of looking like abrupt scheduler crashes or silent exits.
The original root demo remains available in demo.py as a compatibility wrapper around demos/runtime_demo.py.
smallOS is intentionally small in scope:
- tasks are
async defcoroutines wrapped inSmallTask - task code awaits smallOS-owned awaitables such as
task.sleep(...),task.wait_signal(...),task.wait_readable(...), andtask.join(...) - the scheduler steps coroutines directly and decides when each task becomes runnable again
- kernels provide timing, output, and readiness-based transport primitives
This means arbitrary asyncio libraries are not drop-in compatible with the
runtime, but it also means scheduling policy and portability stay under your
control.
Execution adapters let a SmallOS task yield while user-supplied code runs under a different execution model. They use only the Python standard library and do not install, import, configure, or wrap database drivers, ORMs, SDKs, or other third-party packages.
Use ThreadAdapter for a synchronous blocking callable:
from SmallPackage.adapters.threads import ThreadAdapter
def load_record(user_library, settings, record_id):
connection = user_library.connect(**settings)
try:
return connection.load(record_id)
finally:
connection.close()
with ThreadAdapter(max_workers=4, max_pending=64) as blocking:
async def load(task):
return await blocking.call(
load_record,
user_selected_library,
connection_settings,
42,
)
runtime.fork(SmallTask(2, load, name="load"))
runtime.start()Use AsyncioAdapter for an async callable that must run on asyncio. The
adapter owns one persistent event loop in a dedicated thread, allowing
loop-affine clients to be reused when all their operations are routed through
the same adapter:
from SmallPackage.adapters.asyncio_loop import AsyncioAdapter
async def fetch_record(user_library, settings, record_id):
async with user_library.Client(**settings) as client:
return await client.fetch(record_id)
with AsyncioAdapter(max_pending=64) as foreign_async:
async def load(task):
return await foreign_async.call(
fetch_record,
user_selected_async_library,
connection_settings,
42,
)
runtime.fork(SmallTask(2, load, name="load"))
runtime.start()Important behavior:
- adapters bind to the first SmallOS runtime that uses them;
- adapter shutdown is explicit, so a context manager should wrap
runtime.start(); max_pendingrejects excess work withAdapterCapacityErrorinstead of blocking the scheduler;- cancelling a SmallTask can cancel queued thread work, but cannot forcibly stop a running Python thread;
- asyncio cancellation is requested on the adapter loop, but a user library may delay or suppress it;
- pass an async callable to
AsyncioAdapter.call(), not aTaskorFuturealready owned by another loop; - inspect
AsyncioAdapter.shutdown_errorafter shutdown when application diagnostics need to detect an unexpected loop stop or library teardown failure; normal shutdown leaves it asNone; - use
ThreadAdapter(max_workers=1)when user resources require a serialized, thread-affine execution lane; create, use, and close those resources through calls on that same adapter rather than creating them on the SmallOS thread.
All adapter demos run without installing anything beyond SmallOS:
python3 demos/adapters_demo.py
python3 demos/adapters_sqlite_demo.py
python3 demos/adapters_asyncio_demo.py- demos/adapters_demo.py runs both adapters beside an ordinary cooperative SmallOS task.
- demos/adapters_sqlite_demo.py creates, uses,
and closes an in-memory
sqlite3connection throughThreadAdapter(max_workers=1). This is the ownership pattern to adapt for a thread-affine PostgreSQL driver or ORM session supplied by the user. - demos/adapters_asyncio_demo.py creates an
asyncio.Queue,Futureobjects, and a background task, performs multiple operations, and cleans them up on the same persistent adapter event loop.
For MicroPython targets, the intended flow is:
- choose
ESP32,PicoW, orbuild_micropython_kernel() - optionally connect Wi-Fi through the kernel helper
- build
SmallOS(config=...) - fork tasks and start the runtime
The kernel layer is deliberately generic. Protocol clients such as HTTPS, Redis, MQTT, RabbitMQ/AMQP, and Kafka should be built on top of the shared TCP/TLS socket surface rather than requiring protocol-specific kernel methods.
The current setup now includes first-party smallOS-native helpers for HTTP,
Redis, and MQTT, so users can stay inside the smallOS scheduler instead of
dropping down to raw sockets or depending on asyncio-owned clients.
Available helpers:
SmallHTTPClientSmallRedisClientSmallMQTTClient
Current scope:
- HTTP: request/response helper with query params, JSON bodies, TLS, and chunked/content-length response parsing
- Redis: RESP command execution plus helpers like
ping,get,set,delete,publish, andsubscribe - MQTT: MQTT 3.1.1 connect/disconnect, publish at QoS 0/1/2, subscribe at QoS 0/1/2, and inbound message receive with PUBACK/PUBREC/PUBREL/PUBCOMP handling as required by the protocol
- Both clients: optional username/password auth and TLS transport setup, with
Unix support for custom CA and client certificate paths through
tls_ca_file,tls_cert_file, andtls_key_file
For detailed examples, constructor options, response helpers, and transport notes, see SmallPackage/clients/README.md.
Contributions, issues, experiments, and board-port notes are welcome. Good areas for contribution include:
- new MicroPython port validation
- higher-level protocol clients built on the transport layer
- shell and debugging tools
- more board demos and deployment examples
- additional scheduler tests and edge-case coverage
This project is licensed under the MIT License. See LICENSE.