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.tomlfor modern packaging - How to test, lint, and type-check your library
- How to publish to both TestPyPI and PyPI
⚙️ Step 1: Plan the SDK
| Question | Example |
|---|---|
| Package name | sync_openweatherapi_python_sdk |
| Import name | openweather |
| Core purpose | Simple Python wrapper for OpenWeatherMap API |
| Dependencies | requests, pydantic, python-dotenv |
| Testing tools | pytest, 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, andmypybefore 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
