Courses/Python/Packaging & PyPI

    Advanced Lesson

    Packaging & Publishing Python Libraries to PyPI

    Learn how to turn your Python code into a real, installable package that anyone can use with pip install.

    ๐Ÿ“ฆ What You'll Learn

    This lesson teaches you how to publish professional Python packages:

    • Structuring a library properly
    • Creating pyproject.toml configuration
    • Building wheels and source distributions
    • Uploading to TestPyPI and PyPI with twine
    • Versioning and metadata best practices
    • Making your package discoverable and trustworthy

    After this lesson, anyone can install your code with:

    pip install your-library-name

    ๐Ÿ“ฅ Python Download & Setup

    Download Python from: python.org/downloads

    Latest version recommended (3.11+)

    Part 1: Package Structure & Building

    1. What Is a Python Package?

    ๐Ÿ“ฆ Real-World Analogy:

    Think of a package like a product in a box. The box (folder) contains everything needed: the product itself (your code), instructions (README), warranty info (LICENSE), and a label with specs (pyproject.toml). Anyone can "buy" your product with pip install!

    A package is just a directory with an __init__.py file that can be imported.

    Example structure:

    Package Structure

    Try it Yourself ยป
    Python
    my_cool_lib/
     โ”œโ”€ src/
     โ”‚   โ””โ”€ my_cool_lib/
     โ”‚       โ”œโ”€ __init__.py
     โ”‚       โ”œโ”€ core.py
     โ”‚       โ””โ”€ utils.py
     โ”œโ”€ tests/
     โ”œโ”€ pyproject.toml
     โ”œโ”€ README.md
     โ”œโ”€ LICENSE
     โ””โ”€ .gitignore
    File/FolderPurposeRequired?
    src/my_cool_lib/Your actual library codeโœ… Yes
    pyproject.tomlBuild config & metadataโœ… Yes
    README.mdDocumentation (shown on PyPI)โœ… Highly recommended
    LICENSEUsage permissionsโœ… Highly recommended
    tests/Unit testsOptional but best practice

    The src/ layout is recommended because it catches import mistakes early.

    2. Basic Code Layout

    Minimal library example:

    src/my_cool_lib/__init__.py:

    Python
    __all__ = ["say_hello", "__version__"]
    
    from .core import say_hello
    
    __version__ = "0.1.0"

    src/my_cool_lib/core.py:

    Python
    def say_hello(name: str) -> str:
        return f"Hello, {name}!"

    Users will be able to do:

    Python
    from my_cool_lib import say_hello
    
    print(say_hello("World"))

    3. The Modern Way: pyproject.toml

    pyproject.toml is now the standard way to describe:

    • Project metadata (name, version, description)
    • Build backend (like setuptools)
    • Dependencies

    Minimal example using setuptools:

    Python
    [build-system]
    requires = ["setuptools>=61.0", "wheel"]
    build-backend = "setuptools.build_meta"
    
    [project]
    name = "my-cool-lib"
    version = "0.1.0"
    description = "A tiny example Python library."
    readme = "README.md"
    requires-python = ">=3.9"
    license = { text = "MIT" }
    authors = [
      { name = "Jane Developer", email = "jane@example.com" }
    ]
    dependencies = [
      "requests>=2.31.0",
    ]
    
    [project.urls]
    "Homepage" = "https://github.com/yourname/my-cool-lib"
    "Bug Tracker" = "https://github.com/yourname/my-c
    ...

    Key points:

    • name โ†’ must be unique on PyPI
    • version โ†’ follows semantic versioning (e.g. 0.1.0)
    • requires-python โ†’ minimum Python version you support
    • dependencies โ†’ packages installed when someone installs your library

    4. Semantic Versioning (How to Version Properly)

    ๐ŸŽฏ Real-World Analogy:

    Think of versions like software updates on your phone. A PATCH (15.0.1 โ†’ 15.0.2) fixes bugs quietly. A MINOR update (15.1) adds features. A MAJOR update (15 โ†’ 16) might change everything and require you to relearn things โ€” that's a breaking change.

    Use Semantic Versioning:

    MAJOR.MINOR.PATCH

    e.g. 1.4.2

    PartWhen to BumpExample
    PATCHBug fixes, no breaking changes0.1.0 โ†’ 0.1.1
    MINORNew features, backwards compatible0.1.1 โ†’ 0.2.0
    MAJORBreaking changes0.2.0 โ†’ 1.0.0

    Store the version in one place (e.g. __init__.py) and keep pyproject.toml in sync or use a tool like setuptools-scm later.

    5. Writing a Good README for PyPI

    Your README.md becomes the landing page on PyPI.

    Include:

    • Short description
    • Installation instructions
    • Basic usage example
    • Features list
    • Links (docs, repo, issues)

    Example skeleton:

    README.md Template

    Try it Yourself ยป
    Python
    # my-cool-lib
    
    A small Python library that says hello.
    
    ## Installation
    
    ```bash
    pip install my-cool-lib
    ```
    
    ## Quick Start
    
    ```python
    from my_cool_lib import say_hello
    
    print(say_hello("World"))  # โ†’ "Hello, World!"
    ```
    
    ## Features
    
    - Simple API
    - Type hints
    - Tested with pytest

    Make sure readme = "README.md" is set correctly in pyproject.toml.

    6. Building the Distribution (Wheel + sdist)

    ๐Ÿž Real-World Analogy:

    Building a package is like baking bread. sdist is like shipping the recipe and ingredients โ€” the customer bakes it themselves. Wheel is like shipping the finished loaf โ€” ready to eat immediately. Most people prefer the wheel (faster to install)!

    Install build tools:

    Python
    pip install build

    Then from your project root (same folder as pyproject.toml):

    Python
    python -m build

    This will create:

    Python
    dist/
     โ”œโ”€ my_cool_lib-0.1.0.tar.gz      # Source distribution (sdist)
     โ””โ”€ my_cool_lib-0.1.0-py3-none-any.whl  # Wheel (built package)
    TypeFile ExtensionInstall SpeedWhen Used
    Wheel.whlโšก FastDefault installation
    sdist.tar.gz๐Ÿข SlowerFallback, building from source

    7. Uploading to TestPyPI (Safe Practice Run)

    Before using the real PyPI, publish to TestPyPI.

    Install twine:

    Python
    pip install twine

    Upload to TestPyPI:

    Upload to TestPyPI

    Try it Yourself ยป
    Python
    twine upload --repository-url https://test.pypi.org/legacy/ dist/*

    You'll be asked for:

    • username: often __token__ if using API tokens
    • password: your TestPyPI API token

    Then test install it in a fresh venv:

    Test Install from TestPyPI

    Try it Yourself ยป
    Python
    python -m venv test-env
    source test-env/bin/activate  # or .\test-env\Scripts\activate on Windows
    
    pip install -i https://test.pypi.org/simple/ my-cool-lib

    Check it works:

    Verify Installation

    Try it Yourself ยป
    Python
    from my_cool_lib import say_hello
    print(say_hello("World"))

    If everything looks good, you're ready for real PyPI.

    8. Publishing to the Real PyPI

    1. Create an account on pypi.org
    2. Generate an API token
    3. Configure ~/.pypirc (optional but helpful):

    .pypirc Configuration

    Try it Yourself ยป
    Python
    [distutils]
    index-servers =
        pypi
    
    [pypi]
      username = __token__
      password = pypi-xxxxxxxxxxxxxxxxxxxxxxxxxxxx

    Then upload:

    Python
    twine upload dist/*

    After a successful upload, users can:

    User Installation

    Try it Yourself ยป
    Python
    pip install my-cool-lib

    And import it like any other library.

    9. Updating Your Package (New Versions)

    When you change the code:

    1. Bump the version (e.g. 0.1.0 โ†’ 0.1.1) in:
      • pyproject.toml
      • __init__.py (if you mirror the version there)
    2. Rebuild: python -m build
    3. Upload again: twine upload dist/*

    PyPI will reject duplicate versions, so you must use a new version number each time.

    10. Minimal Checklist Before Publishing

    Part 2: Advanced Package Features

    11. Adding Optional Features With "Extras"

    Sometimes you want optional dependencies that users can install only if they need them.

    Example: a core library with an optional cli or dev feature:

    Optional Dependencies

    Try it Yourself ยป
    Python
    [project.optional-dependencies]
    cli = [
      "typer>=0.12.0",
      "rich>=13.0.0",
    ]
    dev = [
      "pytest>=8.0.0",
      "mypy>=1.10.0",
      "ruff>=0.6.0",
    ]

    Users can then install:

    Installing Extras

    Try it Yourself ยป
    Python
    pip install my-cool-lib[cli]
    pip install my-cool-lib[dev]

    This keeps the base install lightweight, while still offering powerful extras for people who want them.

    12. Entry Points & Console Scripts (Installing a CLI)

    You can ship a command-line tool with your package so users get a global command after installing.

    In pyproject.toml:

    Console Script Entry Point

    Try it Yourself ยป
    Python
    [project.scripts]
    mycool = "my_cool_lib.cli:main"

    Then create:

    Python
    # src/my_cool_lib/cli.py
    def main() -> None:
        print("Hello from mycool CLI!")

    After installing:

    Python
    pip install my-cool-lib
    mycool
    # โ†’ Hello from mycool CLI!

    This is how black, pytest, pip, etc. expose commands.

    13. Classifiers: Telling PyPI & Tools What Your Package Supports

    Classifiers inform:

    • Supported Python versions
    • Intended audience
    • License
    • Topic (e.g. web, ML, data)

    In pyproject.toml:

    Python
    [project]
    classifiers = [
      "Programming Language :: Python :: 3",
      "Programming Language :: Python :: 3.10",
      "License :: OSI Approved :: MIT License",
      "Operating System :: OS Independent",
      "Intended Audience :: Developers",
      "Topic :: Software Development :: Libraries",
    ]

    This improves:

    • Discoverability on PyPI
    • Confidence for users (they can see support clearly)
    • Filtering in tools / searches

    14. Handling Dependencies Safely

    Some tips for dependencies:

    1. Pin or semi-pin versions in development, but keep install requirements slightly relaxed.

    Version Constraints

    Try it Yourself ยป
    Python
    [project]
    dependencies = [
      "requests>=2.31,<3.0",
    ]

    In your own dev environment you can use a lock file from tools like pip-tools, Poetry, or uv.

    2. Avoid unnecessary dependencies

    Lean packages are:

    • Easier to install
    • Less likely to break
    • More secure

    3. Put dev-only tools into optional dev extras, not into main dependencies.

    15. Testing Before Publishing

    Before uploading to PyPI, always run tests in a clean environment.

    Example with pytest:

    Testing Before Publishing

    Try it Yourself ยป
    Python
    python -m venv .venv
    source .venv/bin/activate      # Windows: .venv\Scripts\activate
    
    pip install -e ".[dev]"        # editable install + dev extras
    pytest
    • -e โ†’ editable install (imports code directly from src/)
    • .[dev] โ†’ includes dev dependencies from optional-dependencies

    If tests pass in a clean venv, chances are much higher that users won't hit import errors.

    16. Testing Installation Like a Real User

    After building and uploading to TestPyPI, test your package the way a real user would:

    1. Create a fresh venv:

      Create Fresh Venv

      Try it Yourself ยป
      Python
      python -m venv test-env
      source test-env/bin/activate
    2. Install from TestPyPI:

      Install from TestPyPI

      Try it Yourself ยป
      Python
      pip install -i https://test.pypi.org/simple/ my-cool-lib
    3. Open Python and try:
      Python
      from my_cool_lib import say_hello
      print(say_hello("World"))

    If that works, your packaging, imports, and metadata are all aligned correctly.

    17. Common Packaging Mistakes (and How to Avoid Them)

    a) Forgetting __init__.py

    If __init__.py is missing, Python won't treat the directory as a package.

    Required __init__.py

    Try it Yourself ยป
    Python
    src/
      my_cool_lib/
        __init__.py  โ† must exist
        core.py

    b) Wrong where for packages

    If you use the src/ layout, you must tell setuptools:

    Package Discovery

    Try it Yourself ยป
    Python
    [tool.setuptools.packages.find]
    where = ["src"]

    Otherwise your built wheel may contain no code, and imports will fail.

    c) Importing the package from the project root during dev

    Better approach:

    • Use pip install -e . and run scripts via python -m my_cool_lib.something, or
    • Put your scripts under src/ and run through modules

    d) Uploading the same version twice

    PyPI will reject re-uploads of a version. If you made a mistake:

    1. Bump the version (e.g. 0.1.0 โ†’ 0.1.1)
    2. Rebuild with python -m build
    3. Upload again with twine

    18. Keeping Secrets Safe (Do Not Hardcode Keys)

    Never put secrets (tokens, passwords) in your package.

    Bad:

    Bad: Hardcoded Secret

    Try it Yourself ยป
    Python
    API_KEY = "sk-123456"

    Instead:

    • Use environment variables (os.environ["MY_API_KEY"])
    • Or read from config files that are not in the published package
    • Or let users pass keys into your functions/classes

    Anything inside src/ will be visible to anyone on PyPI or GitHub, so treat it as public.

    19. Automating Publishing With CI (Overview)

    Later on, you can automate releases using CI like GitHub Actions.

    General flow:

    1. Tag a release (e.g. v0.2.0)
    2. CI workflow runs:
      • installs dependencies
      • runs tests
      • builds package (python -m build)
      • uploads with twine using a PyPI token stored as a secret

    Very rough example .github/workflows/release.yml structure:

    GitHub Actions Workflow

    Try it Yourself ยป
    Python
    name: Publish to PyPI
    
    on:
      push:
        tags:
          - "v*.*.*"
    
    jobs:
      build-and-publish:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-python@v5
            with:
              python-version: "3.11"
          - run: pip install build twine
          - run: python -m build
          - run: twine upload dist/*
            env:
              TWINE_USERNAME: __token__
              TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}

    After this, releasing is as simple as pushing a tag.

    20. Making Your Package Friendly for Contributors

    A polished package is easier for others to use and contribute to.

    Good practices:

    • Include a CONTRIBUTING.md explaining:
      • how to set up dev environment
      • how to run tests
      • coding style or lint rules
    • Add a simple Makefile or tasks.py with shortcuts
    • Use a linter/formatter (ruff, black, isort) and mention them in dev extras

    21. Summary of the Packaging Workflow

    Full high-level flow:

    1. Create Structure

      Use src/your_package/ layout. Add __init__.py, modules, tests

    2. Describe Project

      Create pyproject.toml with metadata, dependencies, optional extras, scripts

    3. Write Code + Tests

      Implement core logic. Add unit tests with pytest

    4. Build

      python -m build

    5. Test on TestPyPI

      Upload with twine to TestPyPI. Install in a clean venv. Import and run sample code

    6. Publish to PyPI

      Configure PyPI token. twine upload dist/*

    7. Repeat with New Versions

      Bump version for each change. Rebuild and re-upload

    Part 3: Professional Polish & Best Practices

    22. Handling Versioning Properly (Semantic Versioning Made Simple)

    PyPI expects clear, predictable versioning. The best standard is SemVer:

    MAJOR.MINOR.PATCH

    • MAJOR โ†’ breaking changes
    • MINOR โ†’ new features, no breaks
    • PATCH โ†’ bug fixes

    Examples:

    • 1.0.0 โ†’ first stable release
    • 1.1.0 โ†’ added new features
    • 1.1.5 โ†’ bug fixes only

    23. Cross-Platform Compatibility

    Make your package work on Windows, macOS, and Linux.

    Use pathlib for paths:

    Cross-Platform Paths

    Try it Yourself ยป
    Python
    from pathlib import Path
    
    config = Path.home() / ".mycoolconfig"

    This works everywhere.

    24. Adding Type Hints for Better DX (Developer Experience)

    Typed packages are dramatically easier to use.

    You can add:

    • Inline type hints
    • Or a separate py.typed file to mark package typing support

    Add inside: src/my_cool_lib/py.typed

    And declare in pyproject.toml:

    Typed Package Declaration

    Try it Yourself ยป
    Python
    [tool.setuptools.package-data]
    "my_cool_lib" = ["py.typed"]

    This lets editors like VSCode, PyCharm & MyPy give:

    • Autocomplete
    • Type checking
    • Safer refactors

    Typed libraries are considered higher-quality on PyPI.

    25. Adding Documentation (README, Examples, Tutorials)

    A strong PyPI project must have:

    • A clear README
    • Installation instructions
    • Usage examples
    • API documentation

    README.md can include:

    • Badges (version, downloads)
    • Examples
    • Feature list
    • Contribution guide
    • License

    PyPI will display this directly on the project page.

    26. Adding a License (Important for Public Use)

    If you publish to PyPI without a license, companies legally cannot use your code.

    Popular options:

    • MIT License (very permissive)
    • Apache 2.0 (enterprise-friendly)
    • GPL (viral, requires open sourcing forks)

    Add a LICENSE file at project root and reference it in pyproject.toml:

    License Declaration

    Try it Yourself ยป
    Python
    [project]
    license = { file = "LICENSE" }

    Open-source developers expect this.

    27. Keeping Your Package Secure

    PyPI packages must remain safe. Follow these guidelines:

    โš ๏ธ Never include:

    • Secrets
    • API keys
    • OAuth tokens
    • Database passwords
    • Embedded credentials

    โš ๏ธ Avoid risky patterns:

    • Using eval()
    • Arbitrary imports based on user input
    • Downloading code from the internet

    โœ” Use safety tools:

    Python
    pip install bandit
    bandit -r src/
    
    pip install safety
    safety check

    Security matters for your reputation and your package's adoption.

    28. Distributing Both sdist and Wheel (Why It Matters)

    When you run python -m build, you get two types of packages:

    • mycoollib-1.0.0.tar.gz โ†’ source distribution
    • mycoollib-1.0.0-py3-none-any.whl โ†’ wheel

    Why both?

    • sdist = source code (for platforms that build from scratch)
    • wheel = pre-built, faster installs

    Wheels install instantly and are preferred on modern Python.

    29. Supporting Multiple Python Versions

    You choose which Python versions your package supports.

    In pyproject.toml:

    Python Version Requirement

    Try it Yourself ยป
    Python
    [project]
    requires-python = ">=3.9"

    Then use:

    • tox
    • or nox

    to test on multiple interpreters. This makes your package more stable and predictable for users.

    30. Writing a Professional __init__.py

    Your package's public API should be explicitly controlled.

    Example:

    Professional __init__.py

    Try it Yourself ยป
    Python
    from .core import greet, add
    from .utils import logger
    
    __all__ = ["greet", "add", "logger"]

    Benefits:

    • Clear public interface
    • Cleaner imports for users
    • No accidental API exposure

    Good packages have well-designed import paths.

    31. Packaging Non-Python Files (Assets, Templates, etc.)

    Some libraries include:

    • Config templates
    • JSON/YAML files
    • Static resources
    • HTML templates

    Use:

    Python
    [tool.setuptools.package-data]
    "my_cool_lib" = ["templates/*.html", "data/*.json"]

    Then access them:

    Accessing Package Data

    Try it Yourself ยป
    Python
    from importlib.resources import files
    
    template = files("my_cool_lib").joinpath("templates/index.html").read_text()

    This is cleaner than bundling file paths manually.

    32. Popular Tools That Improve the Publishing Workflow

    Recommended add-ons:

    1. black โ€” automatic formatting
    2. ruff โ€” ultra-fast linting
    3. mypy โ€” typing enforcement
    4. pre-commit โ€” hooks to auto-format before commit
    5. uv/Pipenv/Poetry โ€” dependency managers

    These make your package "production-grade."

    33. Final Practical Checklist Before Publishing

    Here is the final checklist used by professional Python maintainers:

    If all boxes are checked โ†’ ship it.

    ๐ŸŽ“ Final Summary

    You've now mastered professional Python package publishing to PyPI.

    You can now:

    • Structure a library with src/ layout
    • Configure pyproject.toml with all metadata
    • Build wheels and source distributions
    • Test on TestPyPI before publishing
    • Publish to PyPI with twine
    • Version properly with semantic versioning
    • Add optional extras and CLI tools
    • Keep packages secure and cross-platform
    • Automate releases with CI/CD

    These skills let you publish professional Python libraries used by developers worldwide โ€” just like requests, FastAPI, and thousands of other packages on PyPI.

    ๐Ÿ“‹ Quick Reference โ€” Packaging & PyPI

    Tool / FileWhat it does
    pyproject.tomlModern project metadata and build config
    python -m buildBuild wheel and sdist
    twine upload dist/*Upload to PyPI
    twine check dist/*Validate package before upload
    pip install -e .Install package in editable/dev mode

    ๐ŸŽ‰ Great work! You've completed this lesson.

    You can now package your Python code and publish it to PyPI โ€” making your work installable by anyone in the world with pip.

    Up next: Files & Streams โ€” work with large datasets, binary files, and efficient I/O patterns.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy โ€ข Terms of Service