---
title: "Python SDK Integration Best Practices"
answer: "coolhand-python patches httpx and requests in the interpreter that imports it. That patch doesn't automatically exist in every worker process, survives fork() but not spawn(), and can interact with other instrumentation or logging setup. This guide covers the process-model and ordering pitfalls that leave Python integrations silently unmonitored."
category: "Logs \u0026 API"
tags: []
published: 2026-09-22T14:26:53Z
updated: 2026-09-13T16:14:40Z
canonical_url: https://coolhandlabs.com/help/python-sdk-integration-best-practices
index_url: https://coolhandlabs.com/help.md
---

# Python SDK Integration Best Practices

coolhand-python patches httpx and requests in the interpreter that imports it. That patch doesn't automatically exist in every worker process, survives fork() but not spawn(), and can interact with other instrumentation or logging setup. This guide covers the process-model and ordering pitfalls that leave Python integrations silently unmonitored.

`import coolhand` auto-initializes monitoring and patches `httpx.Client.send`/`httpx.AsyncClient.send` and `requests.Session.send` in whichever Python interpreter runs that import. That single sentence is also the source of every pitfall below: the patch lives in one interpreter's memory, so anything that changes *which* interpreter is actually making your AI calls — worker processes, forking strategy, or another library patching the same functions — can put you back on unpatched code without any error.

## Re-import Coolhand in every worker process, not just your entrypoint

**Why it matters:** Background job frameworks (Dramatiq, Celery, RQ) and process-based servers (Gunicorn with process workers) commonly start worker processes with a fresh Python interpreter — for example, invoking `python -m dramatiq myapp`. A worker process started this way does not inherit the `import coolhand` patch from your main application process; it begins completely unpatched, because it's a different interpreter, not a different thread.

```python
# ❌ Broken — coolhand is imported in main.py, but dramatiq workers
# start a fresh interpreter and never import main.py at all
# main.py
import coolhand
from myapp.actors import send_notification  # workers never load this path

# myapp/actors.py
import dramatiq

@dramatiq.actor
def send_notification(...):
    ...  # AI calls here are never intercepted
```

```python
# ✅ Fixed — import coolhand at the top of the module dramatiq actually
# re-imports inside each worker process
# myapp/actors.py
import coolhand  # runs in every worker process, since dramatiq imports this module directly
import dramatiq

@dramatiq.actor
def send_notification(...):
    ...  # ✅ intercepted
```

Coolhand also reads `COOLHAND_API_KEY` from the environment at import time — make sure that variable is actually set in the worker process's environment, not just the parent process that spawns it.

## Know whether your workers `fork()` or `spawn()`

**Why it matters:** Coolhand's patch survives `fork()` (the child process inherits the parent's already-patched memory) but not `spawn()` (a fresh interpreter, same as the process-worker case above). This matters when choosing a `multiprocessing` start method or a Gunicorn worker class — a worker model that spawns rather than forks needs the same "import coolhand in the worker module" fix as above, even if the app "looks" like a single codebase.

## Test your actual patch ordering, not just the happy path

**Why it matters:** If your app also uses other instrumentation that patches the same HTTP libraries — most commonly OpenTelemetry's `httpx`/`requests` auto-instrumentation — the order those patches get applied in can determine whether Coolhand's capture actually fires, or whether it gets wrapped/bypassed by the other library's patch. Don't assume the common case (Coolhand initialized first) is the only case your app will hit in production; explicitly test the order you're least confident about.

## Pass `silent=True` if you use structured logging

**Why it matters:** By default, Coolhand's setup can call `logging.basicConfig`, which is fine for apps using the standard `logging` module but will clobber configuration for apps using structured logging (e.g. `structlog`). If your app configures its own logging pipeline, initialize Coolhand with `silent=True` (or set `COOLHAND_SILENT=true`) so it doesn't touch global logging config:

```python
# ✅ Don't let Coolhand's default logging setup override structlog's configuration
from coolhand import Coolhand

Coolhand(api_key=..., silent=True)
```

## Verify interception in CI, not just a 200 response

An example app or integration test returning a correct response proves your AI provider integration works — it does not prove Coolhand captured it. All three process-model pitfalls above produce a fully working app with silently missing logs. Assert programmatically that a request was actually intercepted (e.g. check `httpx_interceptor.is_patched()` *and* that a mocked call was actually captured) rather than relying on the app returning success.

## Related

- [Node.js/JavaScript SDK Integration Best Practices](https://coolhandlabs.com/help/nodejs-sdk-integration-best-practices.md)
- [Ruby SDK Integration Best Practices](https://coolhandlabs.com/help/ruby-sdk-integration-best-practices.md)


---

Related:
- [Node.js/JavaScript SDK Integration Best Practices](https://coolhandlabs.com/help/nodejs-sdk-integration-best-practices.md)
- [Ruby SDK Integration Best Practices](https://coolhandlabs.com/help/ruby-sdk-integration-best-practices.md)
- [What every field on an LLM request log means](https://coolhandlabs.com/help/llm-request-log-fields.md)
- [When should I use batch requests?](https://coolhandlabs.com/help/how-to-use-batch-requests.md)

[← All help articles](https://coolhandlabs.com/help.md) · [Logs & API](https://coolhandlabs.com/help/category/logs-and-api.md)
