← Back to Blog

A Developer's Guide to Building Zero-Dependency Python Libraries

The Python ecosystem has a culture of composability — pull in the package you need, let the package manager resolve its dependencies, and ship. It's productive, and most of the time it's the right call. But there's a category of library where adding even one dependency is the wrong trade, and that category is larger than most developers assume. I maintain several open-source Python libraries that are strictly zero-dependency — they import nothing outside the standard library — and the benefits have been substantial enough that I now default to this approach for anything general-purpose. Here's why it matters, and the concrete patterns that make it practical.

What "zero-dependency" actually means

A zero-dependency library is one whose install brings in no third-party packages at all — not "just one small dependency," but genuinely none. You can verify this trivially: after pip install, the only new files on your system are the library's own. The Python standard library doesn't count, because it ships with Python itself and is maintained by the same people who maintain the language.

The distinction sounds pedantic until you've debugged a transitive-dependency conflict at 11 p.m. A library that depends on requests, which depends on urllib3, which depends on a specific six for Python 2 compatibility, pulls four packages into your environment — and any one of them can break you when it releases a new major version. "Just one dependency" is almost never one package; it's a subtree.

Why it's worth the discipline

Security you can actually audit

Every third-party package you install is code written by someone else, running with your program's privileges. The larger the dependency tree, the larger the attack surface — and supply-chain attacks via compromised packages are a real, recurring threat. A zero-dependency library shrinks that surface to just its own source, which a careful reviewer can read end to end in an afternoon. When I publish a library that handles configuration or data parsing, I want users to be able to satisfy themselves that it's safe without trusting an opaque web of packages they've never looked at.

Fewer breaking changes

Every external dependency is a version pin you have to maintain. When that dependency releases a backwards-incompatible major version, your library either breaks or forces you to do compatibility work. A zero-dependency library depends only on the Python standard library, which follows a careful, documented deprecation cycle measured in years. I have libraries written against Python 3.8 that still run unchanged on 3.12 — not because I'm a genius, but because the stdlib moves slowly and predictably.

Faster, smaller installs

Installing a zero-dependency library is a single, fast operation with no resolver to run and no subtree to fetch. That matters in CI pipelines, Docker builds, and constrained environments. It also means no version conflicts to resolve against whatever else is in the user's environment — your library slots in cleanly anywhere a compatible Python runs.

The standard library is bigger than you think

The most common reason developers reach for a dependency is the assumption that some task "needs" a package. In practice, the standard library covers a remarkable range, and re-acquainting yourself with it is the highest-leverage thing you can do for writing zero-dependency code. A few areas where I regularly see unnecessary third-party packages:

Concrete patterns

Config loading without a config library

Configuration loading is a classic case where developers pull in TOML or YAML parsers. Since Python 3.11, tomllib is in the standard library, so TOML config is free. For earlier versions, configparser handles INI files natively. A configuration loader I maintain is built entirely on tomllib plus pathlib, and it's under 150 lines:

import tomllib
from pathlib import Path
from dataclasses import dataclass

@dataclass
class AppConfig:
    api_key: str
    timeout: int = 30

def load_config(path: str | Path) -> AppConfig:
    p = Path(path)
    with p.open("rb") as f:
        data = tomllib.load(f)
    return AppConfig(
        api_key=data["api"]["key"],
        timeout=data.get("api", {}).get("timeout", 30),
    )

No external parsing library, no validation framework, no dependency footprint. The dataclass gives type safety, tomllib does the parsing, and the loader is a pure function you can test in isolation.

Validation without a schema library

For lightweight validation, a small helper using type hints and explicit checks is often enough:

def validate_positive(value, name="value"):
    if not isinstance(value, (int, float)) or value <= 0:
        raise ValueError(f"{name} must be a positive number, got {value!r}")
    return value

This won't replace a full schema validator for complex nested structures, and I'm honest about that — for those cases a dependency may be justified. But for the long tail of "is this a positive integer, is this a non-empty string, is this a valid URL," inline checks keep the library dependency-free and the intent obvious at the call site.

Estimating costs without a financial library

One of my libraries estimates LLM API costs. The math is just token counts multiplied by per-token prices. There's no reason that needs a dependency — it's arithmetic. Keeping it stdlib-only means a user can install it in any environment without worrying about conflicts, and audit the pricing logic in a single read.

HTTP without requests

For simple GET/POST, urllib.request is perfectly adequate:

import urllib.request
import json

def fetch_json(url: str, timeout: int = 30) -> dict:
    req = urllib.request.Request(url, headers={"User-Agent": "my-lib/1.0"})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode())

def post_json(url: str, data: dict, timeout: int = 30) -> dict:
    payload = json.dumps(data).encode()
    req = urllib.request.Request(
        url,
        data=payload,
        headers={"Content-Type": "application/json", "User-Agent": "my-lib/1.0"},
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode())

This covers 80% of what people use requests for. For streaming, retries, or complex auth, a dependency is justified — but start without.

When a dependency is the right call

I don't want to overstate the case. There are domains where pulling in a well-maintained package is unambiguously correct: a real ORM over hand-rolled SQL, a battle-tested cryptography library when the standard library's primitives don't cover your needs, a parser generator for a complex grammar. The discipline isn't "never use dependencies" — it's "default to none, and justify each one you add." When I do add a dependency, I document why in the README, so the choice is visible and reversible rather than accidental.

The test I apply: does this dependency solve a problem I genuinely can't solve in a reasonable amount of stdlib code, or am I reaching for it out of habit? Far more often than not, the honest answer is habit.

The mindset shift: treating dependencies as costs rather than conveniences. Each one is a small ongoing tax — on security, on maintenance, on install time, on compatibility. A zero-dependency library pays none of those taxes, and for general-purpose code that others will depend on, that's a gift to the people who use it.

Making it sustainable

The practical cost of zero-dependency development is that you write more of the glue code yourself. I keep this manageable by building a small internal set of patterns I reuse across libraries — the config loader above, a few validation helpers, a consistent error hierarchy — so the "writing it myself" tax is paid once and amortized. And because the standard library is stable, code I write today tends to keep working for years without the churn that dependency-heavy libraries endure.

Try it on your next library

If you're starting a new Python library, try a simple experiment: commit to zero third-party dependencies for the first version. Each time you're about to add one, write down what problem it solves and whether the standard library can solve it in ten or twenty lines. You'll likely find, as I did, that the vast majority of "necessary" dependencies dissolve on contact with that question — and what remains is a library that's faster to install, easier to audit, and far less likely to break someone's environment six months from now.

The best dependency is the one your users never have to install.

Migration path for existing libraries

If you already have a dependency-heavy library, you don't need to rewrite it all at once. Start by identifying the "leaf" dependencies — those that nothing else in your codebase depends on — and replace them one at a time. Run your test suite after each replacement. The goal isn't purity; it's reducing the surface area of things that can break your users.