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.

python
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.

Diagram

๐Ÿ“ฆ import praisonaiagents

๐Ÿงต No thread spawned

๐Ÿ”ง Env vars via setdefault

๐Ÿ“ asyncio logger untouched

๐Ÿชต Root handlers preserved

โš ๏ธ warnings.warn untouched

โœ… Safe fork / subprocess spawn

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.

  1. Host environment variables are preserved

    praisonaiagents._logging._configure_environment() uses os.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.

  2. The asyncio logger is left alone

    "asyncio" was removed from the SDK’s noisy-logger list. Import no longer forces logging.getLogger("asyncio") to CRITICAL, so host asyncio errors (Task exception was never retrieved, scheduling failures) continue to surface.

  3. No cleanup thread is spawned at import

    The runtime/resolve.py cleanup daemon starts lazily on the first cache write โ€” inside resolve_runtime under _runtime_cache_lock โ€” not at module import. A bare import praisonaiagents or a touch of runtime.SessionContext spawns 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")

    See Runtime Resolution โ†’ Background cleanup thread.

Host logging, warnings, and env on import (PR #6135)

PR #6135 removed the last import-time changes to host process state.

  1. 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.

  2. Host httpx / openai / pydantic loggers are kept

    When the host has configured logging, import no longer clears handlers or propagate on httpx, httpcore, openai.*, pydantic, or markdown_it loggers.

  3. warnings.warn is never replaced

    warnings.warn and warnings.warn_explicit are no longer monkey-patched. Noisy third-party warnings are silenced with scoped warnings.filterwarnings(...) entries only, so warnings.warn is the same object before and after import.

  4. Env defaults wait for the LLM layer

    LITELLM_, HTTPX_, and PYDANTIC_* defaults are applied when praisonaiagents.llm is imported โ€” not on import praisonaiagents. Subprocesses the host spawns before using the LLM path inherit an unchanged environment. Values are still applied with setdefault.

Verify the invariant yourself:

python
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_before

The 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)

bash
# --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
python
# 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

python
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

python
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.

Related