PyPI Package: Complete Guide for Craft Easy API¶
Date: 2026-03-27 Audience: You — no prior PyPI experience required Related: Settings Reference
Table of Contents¶
- What is PyPI exactly?
- Private or public — and when?
- Step by step: From zero to package
- Version management
- Development workflow
- Publishing
- Community — if and when you open up
- Common mistakes
- Checklist
1. What is PyPI?¶
PyPI (Python Package Index) is Python's central package registry — like npm for JavaScript or NuGet for .NET.
Mental model¶
You write code
│
▼
You package the code (pyproject.toml describes the package)
│
▼
You publish to a registry
│
├─ PyPI (public) → anyone can install
├─ TestPyPI (test) → public but for testing
├─ GitHub Packages → private, tied to your GitHub
└─ Local → pip install -e . (just you)
│
▼
Other projects install the package
│
pip install craft-easy-api
What happens during pip install?¶
- pip contacts PyPI (or your registry)
- Finds the latest version matching the requirements
- Downloads a
.whlfile (zip containing your code) - Extracts it into
site-packages/ - Now the project can do
from craft_easy import ...
There is no magic — it is just your code being copied to the right location.
2. Private or public — and when?¶
Recommendation: Start private, open up when mature¶
Phase 1 (now → v1.0) Phase 2 (v1.0 → v2.0) Phase 3 (v2.0+)
───────────────── ───────────────────── ──────────────
PRIVATE PUBLIC (PyPI) COMMUNITY
but passive active
- Git install - pip install - Contributions welcome
- Only you use it - Documentation ready - Issues, PRs
- API unstable - API stable - Discord/Discussions
- Breaking changes OK - Semantic versioning - Roadmap
- No obligations - License (MIT) - Contributors guide
Why not public right away?¶
| Risk | Consequence |
|---|---|
| API changes frequently | Others depend on unstable API → frustrated users |
| Security vulnerabilities | Publicly exposed before you have had time to review |
| Support burden | Issues from strangers before you are ready |
| Name change | Difficult to change the package name after publishing |
Why not private forever?¶
| Benefit of public | Detail |
|---|---|
| Easier installation | pip install craft-easy-api without config |
| Community contributions | Others find bugs, suggest features |
| CV/portfolio | Shows that you built a framework |
| Motivation | External users provide energy |
Recommendation¶
| Phase | When | Action |
|---|---|---|
| 1. Private | Now → until v1.0 works in 2+ projects | Install via git+https://github.com/... |
| 2. PyPI publishing | When the API is stable and you are not breaking things every week | pip install craft-easy-api becomes possible |
| 3. Community | When at least 1 external person wants to use it | Open issues, create contributing guide |
3. Step by step: From zero to package¶
Step 1: Create the repo¶
Step 2: Project structure¶
craft-easy-api/
├── src/
│ └── craft_easy/ # Package name (underscore, not hyphen)
│ ├── __init__.py # Version + exports
│ ├── app.py
│ ├── settings.py
│ └── core/
│ └── ...
├── tests/
│ └── ...
├── pyproject.toml # EVERYTHING is configured here
├── README.md
├── LICENSE
└── .gitignore
Important: Use the src/ layout (not flat). This prevents accidentally importing local files instead of the installed package.
Step 3: pyproject.toml¶
This is the only file that describes your package. No setup.py, no setup.cfg, no MANIFEST.in — everything modern uses only pyproject.toml.
[project]
name = "craft-easy-api" # Package name on PyPI
version = "0.1.0" # Current version
description = "Base system for REST APIs"
readme = "README.md"
license = "MIT"
requires-python = ">=3.12"
authors = [
{name = "Patrik", email = "patrik@example.com"},
]
dependencies = [
"fastapi>=0.115",
"beanie>=1.27",
"pydantic>=2.10",
# ... all runtime dependencies
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.8",
# ... only for development
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/craft_easy"]
Step 4: __init__.py¶
# src/craft_easy/__init__.py
"""Craft Easy API — Production-ready base system for REST APIs."""
__version__ = "0.1.0"
# Public API — the only things users should import
from .app import create_base_app
from .settings import BaseSettings
__all__ = ["create_base_app", "BaseSettings", "__version__"]
Step 5: Install locally (editable mode)¶
# In craft-easy-api/
pip install -e ".[dev]"
# Now you can import:
python -c "from craft_easy import create_base_app; print('OK')"
# And all changes in src/ are reflected immediately without reinstall
-e = "editable" — pip creates a symlink instead of copying. You change the code, it is reflected immediately.
Step 6: Use in another project¶
Or via git (if hosted on GitHub):
4. Version management¶
Semantic Versioning (SemVer)¶
MAJOR.MINOR.PATCH
│ │ │
│ │ └── Bug fix (backwards compatible)
│ └──────── New feature (backwards compatible)
└────────────── Breaking change (incompatible)
Examples:
| Version | What happened |
|---|---|
| 0.1.0 | First version (unstable, everything can change) |
| 0.2.0 | New hook system |
| 0.2.1 | Bug fix in cascade delete |
| 0.3.0 | New feature-based permission system |
| 0.3.0 → 0.4.0 | Broke the API for the AccessGroup model |
| 1.0.0 | Stable! The API is frozen — no breaking changes without a major bump |
| 1.1.0 | New OAuth2 provider |
| 1.1.1 | Bug fix |
| 2.0.0 | Broke something intentionally (new major = "upgrade consciously") |
The rule under 0.x¶
Under 0.x (before 1.0) there are no promises. You can break anything in any version. That is why you start at 0.1.0.
When do you release 1.0.0?¶
Do it when: - [x] At least 2 projects use the package without you changing the API every week - [x] You have run it in production (or close to it) - [x] You can describe the public API without hesitation - [x] You have tests covering the public API
Where is the version stored?¶
One place. Not three.
# pyproject.toml — use dynamic version
[project]
dynamic = ["version"]
[tool.hatch.version]
path = "src/craft_easy/__init__.py"
Now pyproject.toml reads the version from __init__.py. One source, zero duplication.
5. Development workflow¶
Daily work¶
# 1. You work in craft-easy-api/
cd craft-easy-api
source .venv/bin/activate
# 2. Make changes in src/craft_easy/...
# 3. Test
pytest
# 4. Lint + format
ruff check src/ tests/ --fix
ruff format src/ tests/
# 5. Commit
git add -A && git commit -m "Add cascade update chain depth limit"
Test in a project that uses the package¶
# Terminal 1: craft-easy-api/ (editable install)
cd craft-easy-api
pip install -e ".[dev]"
# Terminal 2: your-project/ (uses craft-easy-api)
cd your-project
pip install -e ../craft-easy-api # Points to the local repo
pip install -e ".[dev]"
# Now: changes in craft-easy-api/ are reflected immediately in your-project/
# No rebuild, no reinstall
Branching strategy¶
main ← Stable. Every commit should work.
│
├── feat/crud-resource ← New feature
├── fix/cascade-depth ← Bug fix
└── release/0.2.0 ← Prepare release
Keep it simple:
- main = always working
- Feature branches for all work
- Tag every release: git tag v0.1.0
6. Publishing¶
Phase 1: GitHub only (now)¶
No publishing to PyPI. Projects install via git:
# In your-project/pyproject.toml
dependencies = [
"craft-easy-api @ git+https://github.com/easy-software-system/craft-easy-api.git@v0.1.0",
]
@v0.1.0 = specific tag. The project is pinned to exactly that version.
Upgrade:
# Switch to new version
pip install "craft-easy-api @ git+https://github.com/easy-software-system/craft-easy-api.git@v0.2.0"
Phase 2: TestPyPI (before real PyPI)¶
TestPyPI is a separate instance of PyPI — for practicing without consequences.
# 1. Create an account at https://test.pypi.org/account/register/
# 2. Install build tools
pip install build twine
# 3. Build the package
python -m build
# Creates:
# dist/craft_easy_api-0.1.0.tar.gz
# dist/craft_easy_api-0.1.0-py3-none-any.whl
# 4. Upload to TestPyPI
twine upload --repository testpypi dist/*
# Prompts for username + password (or API token)
# 5. Test installation from TestPyPI
pip install --index-url https://test.pypi.org/simple/ craft-easy-api
Phase 3: Real PyPI¶
# 1. Create an account at https://pypi.org/account/register/
# 2. Create an API token at https://pypi.org/manage/account/token/
# Save the token as: PYPI_API_TOKEN
# 3. Build
python -m build
# 4. Publish
twine upload dist/*
# Username: __token__
# Password: pypi-AgEIcH... (your API token)
# 5. Done!
pip install craft-easy-api # Now works for everyone in the world
Automated publishing (GitHub Actions)¶
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published] # Triggered when you create a GitHub Release
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Trusted Publishing (no API token needed!)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build
run: |
pip install build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# Trusted Publishing = no token, no secret
# Configure on PyPI: https://docs.pypi.org/trusted-publishers/
Trusted Publishing = PyPI trusts GitHub Actions directly. No API token needed. You configure it once on pypi.org and then every GitHub Release publishes automatically.
Release flow¶
# 1. Update version
# src/craft_easy/__init__.py: __version__ = "0.2.0"
# 2. Commit + tag
git add -A
git commit -m "Release 0.2.0"
git tag v0.2.0
git push && git push --tags
# 3. Create GitHub Release (UI or CLI)
gh release create v0.2.0 --title "v0.2.0" --notes "## What's new
- Added cascade update chain depth limit
- Fixed ETag validation on PATCH"
# 4. GitHub Actions builds and publishes automatically to PyPI
7. Community — if and when you open up¶
Level 0: Just you (now)¶
- No obligations
- Break whatever you want
- No documentation necessary beyond your own spec
Level 1: Public repo, passive (v1.0)¶
Minimum:
- [ ] README.md with installation + quick start
- [ ] LICENSE (MIT — simplest, most permissive)
- [ ] Tagged versions
- [ ] CI that runs tests (GitHub Actions)
You do not need to respond to issues or accept PRs. Publish and move on.
Level 2: Open for contributions (v2.0+)¶
Add:
- [ ] CONTRIBUTING.md — how to contribute
- [ ] CODE_OF_CONDUCT.md — code of conduct
- [ ] Issue templates (bug report, feature request)
- [ ] PR template
- [ ] GitHub Discussions (enable in repo settings)
- [ ] Changelog (CHANGELOG.md)
Level 3: Active community project¶
- [ ] Separate GitHub organization (e.g.
craft-easy-framework) - [ ] Multiple maintainers
- [ ] Roadmap (GitHub Projects)
- [ ] Discord or Discussions
- [ ] Documentation site (MkDocs or Sphinx)
- [ ] Sponsors (GitHub Sponsors)
You do not need to plan for level 3 now¶
Start at level 0. Move to level 1 when the package works. Levels 2 and 3 happen organically if people like it.
8. Common mistakes¶
1. Publishing too early¶
Mistake: Publish v1.0.0 to PyPI on day 1. Consequence: You break the API the following week. Users get frustrated. Solution: Start at 0.1.0. Publish to PyPI only when the API is stable.
2. Version exists in three places¶
Mistake: Version in __init__.py, pyproject.toml AND setup.cfg.
Consequence: They get out of sync. pip installs the wrong version.
Solution: One source (__init__.py), dynamic version in pyproject.toml.
3. Including everything in the package¶
Mistake: Tests, docs, .env files end up in the pip package.
Consequence: Large package, potential security issues.
Solution: src/ layout + explicit packages = ["src/craft_easy"] in pyproject.toml.
4. Pinning exact versions in a library¶
Mistake: fastapi==0.115.6 (exact version).
Consequence: Conflicts with other packages that need a different version.
Solution: fastapi>=0.115 (minimum). Let the user decide the exact version.
The difference:
# LIBRARY (craft-easy-api) — loose requirements
dependencies = ["fastapi>=0.115"]
# APPLICATION (your-project) — can pin for stability
dependencies = ["craft-easy-api>=1.0", "fastapi==0.115.6"]
5. Forgetting __init__.py¶
Mistake: Missing __init__.py in subpackages.
Consequence: from craft_easy.core.auth import ... → ImportError.
Solution: Every folder that should be importable needs __init__.py (can be empty).
6. Mixing setup.py and pyproject.toml¶
Mistake: Using old guides that say "create setup.py".
Consequence: Confusion, duplicated config.
Solution: Only pyproject.toml. Nothing else. setup.py has been legacy since 2023.
7. Publishing secrets¶
Mistake: .env or private keys in the package.
Consequence: Everyone who installs the package gets your secrets.
Solution: .gitignore + review the dist/ file before uploading.
8. Changing the package name after publishing¶
Mistake: Publish as craft-easy and then want to change to craft-easy-api.
Consequence: Old name is blocked, users are confused.
Solution: Decide on the name before publishing. Check that it is not taken: pip install craft-easy-api (should return "not found").
9. Checklist¶
Before you start coding¶
- [ ] Choose package name — check that it is available on PyPI: https://pypi.org/project/craft-easy-api/
- [ ] Choose license — MIT recommended (simplest, most flexible)
- [ ] Create repo with
src/layout - [ ] Create
pyproject.tomlwith hatchling build backend - [ ] Create
.gitignore(Python template from GitHub) - [ ] Verify:
pip install -e ".[dev]"works
Before sharing with others (Phase 1 → 2)¶
- [ ]
README.mdwith: what it is, installation, quick start, examples - [ ]
LICENSEfile (MIT) - [ ] All tests passing
- [ ]
ruff check+mypyno errors - [ ] Version is 0.x.x (not 1.0)
- [ ] Test publishing on TestPyPI
- [ ] Review the dist/ file — no secrets, no test files
Before v1.0¶
- [ ] At least 2 projects use the package
- [ ] The API has not changed in 2+ weeks
- [ ] Documentation for all public classes/functions
- [ ]
CHANGELOG.mdstarted - [ ] CI: tests + lint + mypy on every push
- [ ] CD: automatic publishing on GitHub Release
Before community (Level 2)¶
- [ ]
CONTRIBUTING.md - [ ]
CODE_OF_CONDUCT.md - [ ] Issue templates (bug, feature)
- [ ] PR template
- [ ] GitHub Discussions enabled
- [ ] At least 1 external user
Quick reference: Commands¶
# === DEVELOPMENT ===
pip install -e ".[dev]" # Install locally (editable)
pytest # Run tests
ruff check src/ --fix # Lint
ruff format src/ # Format
mypy src/ # Type checking
# === BUILD ===
pip install build # Install build tools (once)
python -m build # Build the package → dist/
# === PUBLISH ===
pip install twine # Install upload tools (once)
twine upload --repository testpypi dist/* # TestPyPI
twine upload dist/* # Real PyPI
# === RELEASE ===
git tag v0.1.0 # Tag version
git push --tags # Push tags
gh release create v0.1.0 # Create GitHub Release → triggers CD
# === INSTALL (as a user) ===
pip install craft-easy-api # From PyPI
pip install craft-easy-api==0.2.0 # Specific version
pip install "craft-easy-api @ git+https://github.com/.../craft-easy-api.git@v0.1.0" # From GitHub
pip install -e ../craft-easy-api # Local (development)