Python Import Safety & Fork Behaviour
Import praisonaiagents safely from pre-fork servers, subprocesses, and embedded scripts
import praisonaiagents spawns no background thread, leaves host environment variables alone, preserves host logging handlers, and never replaces warnings.warn โ so it is safe to import inside an existing app or at the parent of a pre-fork server before workers fork.
from praisonaiagents import Agent
# Importing the package spawns nothing and overwrites nothing.
agent = Agent(instructions="You are a helpful assistant")
agent.start("Hello")The user imports the SDK inside a gunicorn master or a multiprocessing parent; nothing starts a daemon thread or corrupts the environment before workers fork.
Reference: PraisonAI PR #5219 (fixes #5162) and PR #6135 (fixes #6134). This is the Python companion to JS Import Safety & Runtimes.
What’s safe on import (after PR #5219)
Three import-time side effects that used to corrupt the host process are gone.
- Host environment variables are preserved
praisonaiagents._logging._configure_environment()usesos.environ.setdefault(key, value). A value the host exported before launch (LITELLM_LOG,LITELLM_TELEMETRY,PYDANTIC_WARNINGS_ENABLED, and the rest) is kept โ and no longer leaks a different value into subprocesses spawned afterwards.python import os import praisonaiagents # If the host exported LITELLM_LOG=DEBUG before launch, it survives import. print(os.environ.get("LITELLM_LOG"))See Logging Configuration โ Environment variables: setdefault, not overwrite for the full variable list.
- The asyncio logger is left alone
"asyncio"was removed from the SDK’s noisy-logger list. Import no longer forceslogging.getLogger("asyncio")toCRITICAL, so host asyncio errors (Task exception was never retrieved, scheduling failures) continue to surface. - No cleanup thread is spawned at import
The
runtime/resolve.pycleanup daemon starts lazily on the first cache write โ insideresolve_runtimeunder_runtime_cache_lockโ not at module import. A bareimport praisonaiagentsor a touch ofruntime.SessionContextspawns nothing, which is exactly the guarantee pre-fork server deployments need.python import praisonaiagents from praisonaiagents.runtime import SessionContext # Neither line starts a background thread โ the daemon waits for the # first resolve_runtime() cache write. ctx = SessionContext(session_id="parent")
Host logging, warnings, and env on import (PR #6135)
PR #6135 removed the last import-time changes to host process state.
- Root logger is left alone if the host configured it
logging.basicConfig(..., force=True)is gone. If the root logger already has a handler, import leaves the root handler list and level unchanged. See Logging Configuration โ When the host has already configured logging. - Host httpx / openai / pydantic loggers are kept
When the host has configured logging, import no longer clears handlers or
propagateonhttpx,httpcore,openai.*,pydantic, ormarkdown_itloggers. - warnings.warn is never replaced
warnings.warnandwarnings.warn_explicitare no longer monkey-patched. Noisy third-party warnings are silenced with scopedwarnings.filterwarnings(...)entries only, sowarnings.warnis the same object before and after import. - Env defaults wait for the LLM layer
LITELLM_,HTTPX_, andPYDANTIC_*defaults are applied whenpraisonaiagents.llmis imported โ not onimport praisonaiagents. Subprocesses the host spawns before using the LLM path inherit an unchanged environment. Values are still applied withsetdefault.
Verify the invariant yourself:
import logging, warnings
class H(logging.Handler):
def emit(self, r): pass
h = H()
logging.getLogger().addHandler(h)
logging.getLogger().setLevel(logging.INFO)
warn_before = warnings.warn
import praisonaiagents # noqa
assert logging.getLogger().handlers == [h]
assert logging.getLogger().level == logging.INFO
assert warnings.warn is warn_beforeThe full assertion set lives in tests/unit/test_import_side_effects.py.
Fork-safety guide
Import at the parent, then fork โ as long as no resolve_runtime call happens before the fork, no daemon thread is cloned into a child in an undefined state.
gunicorn (pre-fork)
# --preload imports your app (and praisonaiagents) once in the master,
# then forks sync workers. Safe after PR #5219: no import-time daemon
# thread is cloned into each worker.
gunicorn --worker-class sync --workers 4 --preload myapp:app# myapp.py โ imported once in the master before fork.
from praisonaiagents import Agent
agent = Agent(instructions="You are a helpful assistant")
def app(environ, start_response):
# resolve_runtime (and the lazy cleanup thread) happens per-worker,
# after fork โ never in the master.
reply = agent.start("Hello")
start_response("200 OK", [("Content-Type", "text/plain")])
return [reply.encode()]multiprocessing.Process
import multiprocessing as mp
from praisonaiagents import Agent # imported in the parent โ safe
def worker(prompt):
agent = Agent(instructions="You are a helpful assistant")
return agent.start(prompt)
if __name__ == "__main__":
procs = [mp.Process(target=worker, args=(f"Q{i}",)) for i in range(4)]
for p in procs:
p.start()
for p in procs:
p.join()ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
from praisonaiagents import Agent # imported in the parent โ safe
def worker(prompt):
agent = Agent(instructions="You are a helpful assistant")
return agent.start(prompt)
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(worker, [f"Q{i}" for i in range(4)]))Best Practices
Import at the parent, resolve in the child
Under a pre-fork server, keep import praisonaiagents in the master and let the first agent.start() / resolve_runtime happen inside each worker. The lazy cleanup daemon then starts per-worker, never in the master.
Configure host logging before or after โ both work
If your app installs root handlers before importing PraisonAI, the SDK leaves them alone. If it imports PraisonAI first, attach your handlers afterwards as usual.
Set env vars before launch to keep them
Export LITELLM_LOG, LITELLM_TELEMETRY, and friends in your shell / systemd / Docker before the process starts. setdefault preserves them and never leaks a different value into subprocesses.
Re-suppress asyncio yourself if you need it
Import no longer silences the asyncio logger. If you want the old quiet behaviour, call logging.getLogger("asyncio").setLevel(logging.CRITICAL) in your own code.