sniffio

A tiny Python library that detects which async framework (trio, asyncio, or curio) your code is currently running under.

Library
PyPI
v1.3.1
154 stars
MIT OR Apache-2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
38 /100 Needs Attention
Development Activity 0
Maintenance 0
Community 72
Maturity 60
Momentum 20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
73 /100 Good
Architecture 70
Code Quality 75
Innovation 55
Learning Curve 90

sniffio solves a narrow but recurring problem for library authors who want to support multiple async I/O frameworks without hard-coding a dependency on any one of them. Calling current_async_library() from inside a coroutine returns a string identifying the active runtime, letting a library branch its implementation accordingly instead of forcing callers into a single async stack.

The detection order is deliberate: it first checks a contextvars.ContextVar and a thread-local override (used by frameworks like Trio and Trio-asyncio to explicitly declare themselves), then falls back to sniffing sys.modules for a running asyncio task, and finally checks for a running Curio loop. If none of these match, it raises AsyncLibraryNotFoundError rather than guessing.

Maintained by the Trio project as a shared utility for the async Python ecosystem, sniffio has become a de facto standard dependency pulled in transitively by major HTTP and networking libraries (httpx, anyio, and others) that need to stay framework-agnostic.

What You Get

  • A single current_async_library() function that returns a string ("trio", "asyncio", or "curio") identifying the running async framework
  • An explicit opt-in mechanism via current_async_library_cvar (a ContextVar) and thread_local.name, so frameworks can declare their identity directly rather than relying on sniffing
  • Automatic fallback detection for asyncio (via asyncio.current_task()) and curio (via curio.meta.curio_running()) when no explicit declaration is present
  • A dedicated AsyncLibraryNotFoundError exception raised when called outside any recognized async context, instead of returning None or a wrong guess
  • Zero runtime dependencies and a py.typed marker for full static type-checking support

Common Use Cases

  • HTTP and networking libraries (like httpx and anyio) that expose a single async API surface but need to dispatch to trio- or asyncio-specific primitives internally
  • Async compatibility shims that provide a generic_sleep()-style helper and need to know which underlying sleep implementation to call
  • Testing utilities that must detect the active event loop implementation before setting up fixtures
  • Any library author who wants to support Trio and asyncio users from one codebase without forcing a dependency on either at import time

Under The Hood

Architecture

The entire runtime behavior lives in sniffio/_impl.py, a single module with no internal layering: a module-level ContextVar (current_async_library_cvar) and a threading.local subclass (_ThreadLocal) hold explicit declarations, and current_async_library() checks them in a fixed priority order (thread-local, then contextvar, then asyncio sniffing via sys.modules and asyncio.current_task(), then curio sniffing via curio.meta.curio_running()) before raising AsyncLibraryNotFoundError. There is no dependency injection or configuration surface; the public API is exactly one function plus the two state objects frameworks use to opt in explicitly, so the only thing that would break by changing the core abstraction is the detection order itself.

Tech Stack

Pure standard-library Python: contextvars.ContextVar, threading.local, and sys.modules introspection, with zero runtime dependencies declared in pyproject.toml. The package is built with setuptools (using setuptools_scm for version derivation from git tags) and requires Python 3.10+. Test and docs tooling is isolated into [dependency-groups] (pytest, pytest-cov, curio for tests; sphinx + sphinx_rtd_theme for docs), and .mypy.ini configures strict-ish static typing with targeted ignore_missing_imports for the untyped curio and pytest packages.

Code Quality

Testing uses pytest with pytest-cov for coverage, and _tests/test_sniffio.py covers all four detection paths explicitly (contextvar override, thread-local override, live asyncio task, live curio task) plus the not-found error case, including a second call per branch to exercise caching behavior. Error handling is intentional rather than defensive: AsyncLibraryNotFoundError is a distinct RuntimeError subclass raised deliberately rather than swallowed. The codebase ships a py.typed marker and is checked with mypy, and CI (ci.sh) enforces yapf formatting and mypy --pretty on every push in addition to running the pytest suite with -W error (warnings promoted to failures).

API Design The public surface is deliberately minimal: one function (current_async_library) plus two low-level state hooks (current_async_library_cvar, thread_local) for frameworks that want to declare themselves explicitly rather than be sniffed. There is no configuration object, no classes to instantiate, and no boilerplate — a caller imports one name and calls it. The docstring documents the exact detection table (library, minimum version required, returned string) inline, and the single custom exception type gives callers one clear thing to catch.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers