Have you ever installed a library with pip install and wondered — how do developers publish their own?

In this post, I’ll show you how I built, tested, and published my own Python SDK — a simple, synchronous client for the OpenWeatherMap API — all the way to PyPI.

You’ll learn:

  • How to structure a reusable SDK
  • How to use pyproject.toml for modern packaging
  • How to test, lint, and type-check your library
  • How to publish to both TestPyPI and PyPI

⚙️ Step 1: Plan the SDK

QuestionExample
Package namesync_openweatherapi_python_sdk
Import nameopenweather
Core purposeSimple Python wrapper for OpenWeatherMap API
Dependenciesrequests, pydantic, python-dotenv
Testing toolspytest, responses, mypy, ruff

The goal is developer ergonomics: clear interfaces, typed models, and testability.


🏗 Step 2: Project structure

sync_openweatherapi_python_sdk/
├── openweather/
│   ├── __init__.py
│   ├── client.py
│   ├── models.py
│   ├── exceptions.py
│   ├── endpoints.py
│   └── utils.py
├── examples/
│   └── usage_sync.py
├── tests/
│   └── test_client_sync.py
├── pyproject.toml
└── README.md

🧩 Step 3: The pyproject.toml core

[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "sync_openweatherapi_python_sdk"
version = "0.1.0"
description = "Sync-only Python SDK for OpenWeatherMap"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Nitin S. Kulkarni" }]
dependencies = ["requests>=2.31", "pydantic>=2.7"]

[project.optional-dependencies]
dev = ["pytest>=8", "responses>=0.25", "ruff>=0.6", "mypy>=1.11", "python-dotenv>=1.0"]

[tool.setuptools]
packages = ["openweather"]
license-files = ["LICENSE"]

[project.urls]
Homepage = "https://github.com/nkpythondeveloper/sync_openweatherapi_python_sdk"
Issues = "https://github.com/nkpythondeveloper/sync_openweatherapi_python_sdk/issues"

🧠 Step 4: Implement the client

import requests
from pydantic import BaseModel

class WeatherResponse(BaseModel):
    name: str
    main: dict
    weather: list

class OpenWeatherClient:
    BASE_URL = "https://api.openweathermap.org/data/2.5"

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()

    def get_current_weather(self, city: str):
        url = f"{self.BASE_URL}/weather"
        params = {"q": city, "appid": self.api_key, "units": "metric"}
        response = self.session.get(url, params=params, timeout=10)
        response.raise_for_status()
        return WeatherResponse(**response.json())

🧪 Step 5: Test, lint, type-check

pytest -q
ruff check .
mypy openweather

Use responses to mock HTTP calls — no API hits needed.


🔑 Step 6: Environment variables

OPENWEATHER_KEY=your_api_key_here
from dotenv import load_dotenv
load_dotenv()

Never commit .env — it’s listed in .gitignore.


🚀 Step 7: Build & publish

pip install build twine
python -m build
twine check dist/*

TestPyPI upload

export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="pypi-your-test-token"
twine upload --repository-url https://test.pypi.org/legacy/ dist/*

Real PyPI upload

export TWINE_PASSWORD="pypi-your-real-token"
twine upload dist/*

✅ Live package: https://pypi.org/project/sync-openweatherapi-python-sdk/


🌟 Step 8: Install & use

pip install sync_openweatherapi_python_sdk
from openweather import OpenWeatherClient
client = OpenWeatherClient(api_key="YOUR_KEY")
data = client.get_current_weather(city="Pune")
print(f"{data.name}: {data.main['temp']}°C")

🧭 Step 9: Lessons learned

  • Start simple — ship it, then iterate.
  • Always test on TestPyPI first.
  • Automate pytest, ruff, and mypy before releases.
  • Use version tags (git tag v0.1.0) to track releases.

🪶 Final thoughts

Publishing to PyPI isn’t just about sharing code — it’s about sharing craftsmanship.
Once you’ve done it once, it becomes a five-minute process to ship clean, versioned, installable Python tools.


Author: Nitin S. Kulkarni
Project: sync-openweatherapi-python-sdk
PyPI: sync-openweatherapi-python-sdk