Skip to content

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

  1. What is PyPI exactly?
  2. Private or public — and when?
  3. Step by step: From zero to package
  4. Version management
  5. Development workflow
  6. Publishing
  7. Community — if and when you open up
  8. Common mistakes
  9. Checklist

1. What is PyPI?

PyPI (Python Package Index) is Python's central package registry — like npm for JavaScript or NuGet for .NET.

pip install fastapi      # Fetches from PyPI
pip install craft-easy-api # Same thing, but your package

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?

  1. pip contacts PyPI (or your registry)
  2. Finds the latest version matching the requirements
  3. Downloads a .whl file (zip containing your code)
  4. Extracts it into site-packages/
  5. 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

mkdir craft-easy-api
cd craft-easy-api
git init

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

# In your-project/
pip install -e ../craft-easy-api  # Local reference during development

Or via git (if hosted on GitHub):

pip install "craft-easy-api @ git+https://github.com/easy-software-system/craft-easy-api.git@main"

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.

# src/craft_easy/__init__.py
__version__ = "0.1.0"
# 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)

Private repo → git install → you are the only user
  • No obligations
  • Break whatever you want
  • No documentation necessary beyond your own spec

Level 1: Public repo, passive (v1.0)

Public repo → PyPI → others CAN use it

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+)

Public repo → PyPI → others USE it → they want to contribute

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

Organization → maintainers → contributors → users
  • [ ] 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.toml with hatchling build backend
  • [ ] Create .gitignore (Python template from GitHub)
  • [ ] Verify: pip install -e ".[dev]" works

Before sharing with others (Phase 1 → 2)

  • [ ] README.md with: what it is, installation, quick start, examples
  • [ ] LICENSE file (MIT)
  • [ ] All tests passing
  • [ ] ruff check + mypy no 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.md started
  • [ ] 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)