
5 Best Practices for Building Robust Python AI Libraries
This article covers building robust Python AI libraries specifically, Python AI SDK best practices, and what separates a production-ready AI package from one that only survives in its own demo.
以下正文同步自 KDnuggets,版权归原站所有,已转换为易读排版。
AI libraries have a dependency-weight problem ordinary libraries rarely face. A library offering both a hosted-API path and a local-model path shouldn't force every user into installing torch or transformers just to use the hosted path, and vice versa. The fix is pyproject.toml's optional-dependencies extras mechanism, paired with a lazy import inside the library that fails loudly and helpfully rather than with a bare ModuleNotFoundError.
[project] name = "mylib" dependencies = [ "pydantic>=2.0", "httpx>=0.27", ]
[project.optional-dependencies] openai = ["openai>=1.0"] local = ["torch>=2.0", "transformers>=4.40"] all = ["mylib[openai,local]"]
def _require(module_name: str, extra_name: str): """Import an optional dependency, raising a clear, actionable error naming the exact extra to install if it's missing.""" try: return import(module_name) except ImportError as e: raise ImportError( f"'{module_name}' is required for this feature. " f"Install it with: pip install 'mylib[{extra_name}]'" ) from e
def load_local_model(model_name: str): torch = _require("torch", "local") transformers = _require("transformers", "local") return transformers.AutoModel.from_pretrained(model_name)
The core dependency list in [project] stays deliberately small — just pydantic and httpx, both lightweight. The [project.optional-dependencies] table defines named extras, so pip install mylib[openai] pulls in only what the hosted-API path needs, pip install mylib[local] pulls in the heavier local-inference stack, and pip install mylib[all] gets everything. The _require helper is what makes this genuinely usable rather than just technically correct: without it, a user who skips the local extra and calls load_local_model gets a bare ModuleNotFoundError: No module named 'torch' with no indication of what to do about it. With it, they get an error that names the exact pip install command that fixes the problem — which is the difference between a five-second fix and a confused GitHub issue.
AI libraries live or die on the reliability of a third party they don't control. Providers rate-limit, time out, and occasionally return a 503 that clears up in a few seconds if retried, and code that doesn't account for any of that turns a routine, transient hiccup into a hard failure for every user of the library. Per a real, worked example of this pattern in Machine Learning Plus's resilient large language model client walkthrough, the fix is retry-with-backoff scoped specifically to the errors worth retrying, plus an explicit cap so a struggling provider doesn't turn into an infinite loop.
import logging import httpx from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception_type, before_sleep_log, )
logger = logging.getLogger("mylib")
class ProviderUnavailableError(Exception): """Raised when a provider call fails after all retries are exhausted."""
@retry( stop=stop_after_attempt(3), # hard cap, never retry forever wait=wait_exponential(multiplier=1, min=1, max=10), # 1s, 2s, 4s... capped at 10s retry=retry_if_exception_type((httpx.TimeoutException, httpx.HTTPStatusError)), before_sleep=before_sleep_log(logger, logging.WARNING), reraise=True, # on final failure, raise the real underlying error ) def _call_provider(client, **kwargs): return client.chat.completions.create(timeout=15.0, **kwargs)
Every parameter in that @retry decorator is doing real, deliberate work. stop_after_attempt(3) is the hard ceiling that stops this from ever becoming an unbounded retry loop — a genuinely dangerous failure mode where a struggling provider quietly turns into a runaway bill or a hung process. wait_exponential spaces retries out with increasing delay rather than hammering an already-struggling provider immediately three times in a row. retry_if_exception_type is what keeps this safe: it only retries on genuinely transient failures — timeouts and HTTP errors — and deliberately does not retry on, say, an authentication error, since retrying a bad API key three times wastes time without ever fixing the actual problem. before_sleep_log gives visibility into every retry as it happens rather than silently succeeding or failing with no trace. And reraise=True ensures that when all three attempts genuinely fail, the caller sees the real underlying exception, not a generic "retry library gave up" error that hides what actually went wrong. timeout=15.0 set explicitly on the call itself is worth noting too, since a library that never sets its own timeout is entirely at the mercy of whatever default — or lack of one — the underlying HTTP client happens to ship with.
The first four practices only stay true over time if something enforces them automatically, rather than relying on every contributor remembering to run the linter before pushing. Per the concrete, current tool stack laid out in Stephen Funk's 2026 writeup on building a Python library, the practical 2026 default is uv for environment and dependency management, ruff for both linting and formatting, mypy for type checking, and pytest with coverage for the test suite built in Practice 2 — wired together in a CI workflow that runs before anything gets released.
pyproject.toml, dev dependencies
[dependency-groups] dev = [ "ruff>=0.6", "mypy>=1.11", "pytest>=8.0", "pytest-cov>=5.0", "tenacity>=9.0", ]
.github/workflows/ci.yml
name: CI
on: push: branches: [main] pull_request:
jobs: quality: runs-on: ubuntu-latest steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --all-extras --dev
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy src/
- run: uv run pytest --cov=mylib --cov-report=term-missing tests/
Each step in that workflow exists to catch one specific way a contribution could quietly erode the library's reliability. ruff check and ruff format --check catch style drift and a real class of bugs ruff's linter rules flag directly, and running format --check rather than format fails the build on unformatted code instead of silently reformatting it in CI. mypy src/ verifies that the type annotations Practice 1's Pydantic schemas depend on are actually correct throughout the codebase, not just in the one function someone happened to test by hand. pytest --cov=mylib --cov-report=term-missing runs the exact boundary-mocked test suite from Practice 2 and reports which lines still aren't covered, so a gap in testing shows up as a number in the CI log instead of a surprise in production. uv sync --all-extras --dev pulls in every optional dependency from Practice 3 specifically so CI is testing the full surface of the library, not just whatever subset happens to be installed on one contributor's machine. None of these steps are exotic. What makes them a practice rather than a suggestion is that they run on every single push, automatically, whether or not anyone remembers to ask for them.
A few mistakes come up often enough to name directly, since most of them are the exact failure mode one of the five practices above exists to prevent.
None of these five practices are really about following a checklist. They all answer the same question a library's users will eventually ask under real pressure — when a model returns something unexpected, when a provider has a bad night, when someone installs the package on a machine that can't spare four gigabytes for a dependency they don't need: can I trust this thing when something goes wrong? A library that's already answered that question before it ships, rather than after its first production incident, is the one people actually keep using.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。