Summary
While integrating the PostHog Python SDK into a strict Python client, we hit several quality-gate failures around strict typing and optional SDK initialization. We were able to work around them locally, but the experience was rough enough that I wanted to report it upstream.
The main friction points were:
pyright reported partially unknown types when using the PostHog client object, especially around shutdown.
mypy could not find library stubs for posthog, which forced us to avoid a normal static import.
- Optional initialization paths were easy to make type-checker-hostile because the SDK client type is not available in a typed way.
Environment
- Package:
posthog
- Language: Python
- Type checkers:
- Project using it: Python client
- Python observed in test run:
3.14.3
Errors observed
Pyright
analytics.py: Type of "shutdown" is partially unknown
This happened when registering shutdown with atexit from a lazily initialized PostHog client.
Mypy
analytics.py: Unused "type: ignore" comment
Cannot find implementation or library stub for module named "posthog"
The direct import:
from posthog import Posthog
caused mypy to complain about missing stubs. Adding a type: ignore was not acceptable in our repository's quality policy, and in one run it also became an unused ignore.
Downstream optional-client fallout
After the SDK wrapper passed lint/type checks, our test suite exposed a separate crash caused by analytics code assuming a fully initialized serving client:
AttributeError: 'ServingClient' object has no attribute '_base_url'
That one was our bug, but it was triggered in the analytics call path and made the integration more fragile than expected.
Workaround used
We ended up avoiding the static import and defining a local protocol for the subset of the SDK we use:
from importlib import import_module
from typing import Any, Protocol, cast
class _PostHogClient(Protocol):
def capture(self, *, distinct_id: str, event: str, properties: dict[str, Any]) -> None: ...
def capture_exception(self, exc: BaseException, distinct_id: str) -> None: ...
def shutdown(self) -> None: ...
posthog_module = import_module("posthog")
posthog_class = posthog_module.Posthog
client = cast(_PostHogClient, posthog_class(project_token, **kwargs))
This works, but it is more ceremony than we expected for a small optional analytics integration.
Expected behavior
It would be helpful if the Python SDK shipped enough typing metadata for strict projects to use the normal import path without local protocols/casts:
from posthog import Posthog
client = Posthog(project_token, enable_exception_autocapture=True)
client.capture(...)
client.capture_exception(...)
client.shutdown()
Ideally this should pass both pyright and mypy without downstream projects needing custom stubs, local protocols, or ignore comments.
Why this matters
Strict Python projects often prohibit type: ignore/noqa style suppressions in CI. Without typed exports or bundled stubs, integrating PostHog requires either loosening quality policy or hiding the SDK behind local dynamic-import wrappers.
Summary
While integrating the PostHog Python SDK into a strict Python client, we hit several quality-gate failures around strict typing and optional SDK initialization. We were able to work around them locally, but the experience was rough enough that I wanted to report it upstream.
The main friction points were:
pyrightreported partially unknown types when using the PostHog client object, especially aroundshutdown.mypycould not find library stubs forposthog, which forced us to avoid a normal static import.Environment
posthogpyrightmypy3.14.3Errors observed
Pyright
This happened when registering shutdown with
atexitfrom a lazily initialized PostHog client.Mypy
The direct import:
caused
mypyto complain about missing stubs. Adding atype: ignorewas not acceptable in our repository's quality policy, and in one run it also became an unused ignore.Downstream optional-client fallout
After the SDK wrapper passed lint/type checks, our test suite exposed a separate crash caused by analytics code assuming a fully initialized serving client:
That one was our bug, but it was triggered in the analytics call path and made the integration more fragile than expected.
Workaround used
We ended up avoiding the static import and defining a local protocol for the subset of the SDK we use:
This works, but it is more ceremony than we expected for a small optional analytics integration.
Expected behavior
It would be helpful if the Python SDK shipped enough typing metadata for strict projects to use the normal import path without local protocols/casts:
Ideally this should pass both
pyrightandmypywithout downstream projects needing custom stubs, local protocols, or ignore comments.Why this matters
Strict Python projects often prohibit
type: ignore/noqastyle suppressions in CI. Without typed exports or bundled stubs, integrating PostHog requires either loosening quality policy or hiding the SDK behind local dynamic-import wrappers.