8.2 KiB
Python Library Development Guidelines
Document Version: v5
Note on Versioning:
- This document version is independent — reused across projects
- Project version source of truth:
pyproject.tomlunder[project]CHANGELOG.mduses project version frompyproject.toml
Related Documents
- README.md — Project overview, public API description, installation and usage examples
- AGENTS.md — Rules for AI assistants
- PROJECT.md — Project goals and current state
- CHANGELOG.md — Version history
Documentation Organization
All detailed documentation of features and systems belongs in the docs/ folder, not in the project root.
The root directory contains only the core documents: README.md, CLAUDE.md, AGENTS.md, DESIGN_DOCUMENT_PYTHON_MODULE.md, PROJECT.md, CHANGELOG.md.
1. Code Style
- PEP8 with 120-character lines (Ruff)
- 4 spaces indentation
- snake_case functions/variables, PascalCase classes, SCREAMING_SNAKE_CASE constants
- Type hints required on all functions
- Import order: stdlib → third-party → local
2. SOLID Principles
- SRP — One class = one responsibility
- OCP — Open for extension, closed for modification
- LSP — Subclasses substitutable for parents
- ISP — Small interfaces over large ones
- DIP — Depend on abstractions
3. Dependency Injection
Pass dependencies via constructor. Never instantiate dependencies inside a class.
4. Protocols Over Inheritance
Prefer typing.Protocol and composition over class inheritance.
5. Data Classes
Use @dataclass for internal data structures, pydantic.BaseModel for data that requires validation.
6. Logging
This is a library. Libraries must never configure logging sinks — that is the responsibility of the consuming application.
Usage in library code
from loguru import logger
def some_function() -> None:
logger.debug("Detail message")
logger.info("Milestone message")
Always use the module-level logger from loguru directly. Never call logger.add() or logger.remove() inside library code.
Behavior for consumers
Loguru has a default sink to stderr enabled. Consumers who also use loguru get library logs automatically in their configured sinks. Consumers who want to suppress or filter library logs use the package name as the identifier:
from loguru import logger
logger.disable("<package_name>") # suppress all
logger.add(sys.stderr, filter={"<package_name>": "WARNING"}) # WARNING and above only
logger.enable("<package_name>") # re-enable
This must be documented in README.md.
Rules
- Never call
logger.add()inside library code — no file sinks, no stdout sinks - Never call
logger.remove()inside library code - Never log secrets, passwords, tokens, or API keys
- Never use
print()inside library code — not for debugging, not for output
Log levels
| Level | When to use |
|---|---|
DEBUG |
Per-item detail: individual operations, cache hits, internal state |
INFO |
Significant milestones visible to the consuming application |
WARNING |
Recoverable issues: fallback used, unexpected but non-fatal state |
ERROR |
Failures the caller must know about — operation cannot continue |
7. Environment and Secrets
- Libraries do not use
.envfiles orpython-dotenv - Configuration is passed by the caller via arguments or constructor parameters
- Never read
os.getenv()inside library code unless explicitly documented as a supported configuration mechanism
8. Error Handling
Define specific exception types in src/<package>/exceptions.py. Use a fail-fast approach — surface errors early rather than silently continuing. All public exceptions must be exported from the top-level __init__.py.
9. Testing
- pytest only — no
unittest, noTestCaseclasses, noself.assert* - Arrange-Act-Assert pattern
- Test naming:
test_<action>_<context>
10. Tooling
| Tool | Purpose |
|---|---|
| Ruff | Formatting and linting |
| mypy | Static type checking |
| pytest | Testing |
Run before every commit:
uv run ruff check
uv run mypy
uv run pytest
11. uv
uv sync # Install all dependencies
uv add <pkg> # Add runtime dependency
uv add --dev <pkg> # Add dev dependency
uv remove <pkg> # Remove dependency
uv run <cmd> # Run command in virtualenv
uv build # Build sdist and wheel into dist/
uv publish # Publish to PyPI
uv python install <version> # Install a uv-managed Python
Never edit pyproject.toml directly to add or remove dependencies.
The interpreter is a uv-managed Python — no pyenv, no system Python.
pyproject.toml configuration for a library
[project]
name = "your-library"
version = "0.1.0"
description = "Short description"
requires-python = ">=3.x"
dependencies = []
[build-system]
requires = ["uv_build>=0.13.0,<0.14.0"]
build-backend = "uv_build"
uv_build finds the package at src/<name>/ on its own, with the dashes in the project name turned
into underscores — no package list is needed. Keep the upper bound on uv_build; uv init --lib
writes the range that matches the installed uv.
Do not add [project.scripts] — libraries have no entry points.
uv.lock
Do not commit uv.lock for libraries. It is only committed for applications. Add uv.lock to .gitignore.
12. Project Structure
project/
├── src/
│ └── your_library/
│ ├── __init__.py # Public API — export everything the caller needs
│ ├── exceptions.py # All public exception types
│ └── ... # Internal modules
├── tests/ # Tests
├── docs/ # Detailed documentation
├── .venv/ # Virtual environment (managed by uv)
└── pyproject.toml # Project config and dependencies
- No entry point scripts in the project root — this is a library, not an application
__init__.pydefines the public API; callers import from the top-level package only
13. Public API
- Everything intended for external use must be exported from
src/<package>/__init__.py - Use
__all__to explicitly declare the public surface - Internal modules are prefixed with
_or kept unexported - Public API must be stable across patch versions; breaking changes require a major version bump
14. Distribution
Build and publish with uv:
uv build # Creates dist/*.tar.gz and dist/*.whl
uv publish # Publishes to PyPI (requires credentials)
dist/ is not committed to the repository — add it to .gitignore.
15. Versioning
- Follow semantic versioning:
MAJOR.MINOR.PATCH - Version is defined in
pyproject.tomlunder[project] - Always ask before bumping the version — never increment automatically
- Update
CHANGELOG.mdbefore bumping the version - Breaking changes to the public API require a major version bump
16. Documentation and Task Management
- Keep
PROJECT.mdandCHANGELOG.mdup to date when making changes README.mdmust contain installation instructions and usage examples for the public API- Document architectural changes in this file or in
docs/
Task notation
Tasks are written as single-line comments directly in code, using the Todo Tree tags defined in AGENTS.md (TODO, FIXME, BUG, HACK, NOTE). PROJECT.md carries only cross-cutting tasks that have no single place in the code.
# TODO: extract this into a separate loader
# FIXME: crashes on an empty file
# BUG: rounding is off by one cent on negative amounts
# HACK: temporary workaround until the API adds paging
# NOTE: order matters here, the parser is stateful
No other task format is used — no checkboxes, no numbered lists in documentation.
If a tag already exists at a specific location in code, do not repeat it in PROJECT.md.