Welcome! This lesson moves from pure Python code into the world of software distribution and operations . These are crucial skills for any full-stack or backend developer. We'll cover how the Python packaging ecosystem works, how to publish your own package, how to manage dependencies cleanly, how to log effectively, and the most important topic: how to handle secret credentials safely. Take your time, as these concepts are key to building professional, maintainable applications.
This can seem confusing, but it helps to think of it like building and shipping IKEA furniture .
First, let's define the two main parts of the problem:
Here’s how the tools fit into that analogy:
Key takeaway: Today, the standard is to define your project in pyproject.toml . You can use a simple build-backend like setuptools or an all-in-one tool like Poetry or uv. pip installs wheel files that these tools create.
The rest of the modern toolchain: Ruff
Interviewers often follow packaging with “how do you keep code quality up?”. The answer changed recently. Ruff is a Rust-based linter and formatter that replaces most of the old stack in one tool, and runs fast enough to sit in a pre-commit hook without anyone noticing it.
flake8 + its plugin ecosystem, pylint rules, isort, pyupgrade, bandit → ruff checkblack → ruff format (deliberately black-compatible output)mypy or pyright - Ruff does not do type inference.# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py313"
[tool.ruff.lint]
# pycodestyle, pyflakes, isort, pyupgrade, bugbear
select = ["E", "F", "I", "UP", "B"]
# Terminal
ruff check --fix .
ruff format .Putting it together: a current Python project in 2026 typically looks like uv for environments and dependencies, Ruff for linting and formatting, mypy or pyright for types, and pytest for tests - all configured in a single pyproject.toml.
Publishing a package to PyPI (the Python Package Index) is like shipping your "furniture kit" to the global warehouse so anyone in the world can pip install it.
Here is the modern, step-by-step process:
Example Package Structure:
# This is a comment representing a directory structure
cool_project/
├── src/
│ └── my_awesome_package/
│ ├── __init__.py
│ └── calculator.py
├── pyproject.toml
├── README.md
└── LICENSEExample pyproject.toml:
# This file uses the TOML format
[build-system]
# Tells pip what tools are needed to build your package
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
version = "0.1.0"
authors = [
{ name="Your Name", email="you@example.com" },
]
description = "A small example package"
readme = "README.md"
license = { file="LICENSE" }
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
# This is where you list dependencies
dependencies = [
"requests>=2.20",
"tqdm~=4.0"
]
[project.urls]
Homepage = "https://github.com/you/cool_project"
Issues = "https://github.com/you/cool_project/issues"
Example Build and Upload Commands:
# 1. Install the build tools
pip install build twine
# 2. Run the build process from your project's root directory
python -m build
# This creates a 'dist/' folder with two files:
# dist/my_awesome_package-0.1.0-py3-none-any.whl
# dist/my-awesome-package-0.1.0.tar.gz
# 3. Upload to PyPI (this will prompt for your username and API token)
# For a real project, upload to TestPyPI first!
# twine upload --repository testpypi dist/*
# 4. Upload to the real PyPI
twine upload dist/*
Pro Tip: Always upload to TestPyPI first! It's a separate index for testing your package. This prevents you from uploading broken or test versions to the real PyPI. The command is twine upload --repository testpypi dist/* .
Dependency management is about solving "dependency hell." The problem is simple: Project A needs requests==2.20 , but Project B needs requests==2.28 . If you install them globally, one project will break.
Analogy: A virtual environment is a separate, clean workbench for each project . You get a fresh set of tools (Python, libraries) for each project, so they don't interfere with each other.
Here are the common tools and patterns, from basic to advanced:
Example: venv + pip-tools workflow
# 1. Create a requirements.in file with your direct dependencies
# File: requirements.in
flask
requests>=2.25
# 2. Create and activate your virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# 3. Install pip-tools
pip install pip-tools
# 4. Compile your requirements.txt
pip-compile requirements.in
# This generates a new file...
# File: requirements.txt
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements.in
#
click==8.1.7
# via flask
flask==3.0.0
# via -r requirements.in
...
requests==2.31.0
# via -r requirements.in
...
# 5. Install the exact versions into your venv
pip-sync
My Recommendation: For most web projects, start with venv and pip-tools . If your project is a library you intend to publish, or if you prefer an all-in-one tool, Poetry is the modern standard.
This is a critical topic for production systems.Analogy: Logging is your application's flight recorder or ship's log. It tells you what happened, when, and why, especially after a crash. Monitoring is the live dashboard in the cockpit, showing your speed, altitude, and warnings.
Monitoring is about high-level, aggregate metrics , not individual events.
This is the most important modern logging practice. Instead of logging a plain text string:
Bad (unstructured): User 123 failed to log in from 192.168.1.10
Good (structured): {"timestamp": "...", "level": "WARNING", "event": "login_failed", "user_id": 123, "ip_address": "192.168.1.10"}
Why? The structured log is machine-readable . You can easily send these JSON logs to a service like Datadog, Splunk, or an ELK stack and filter, search, and aggregate them. You can easily make a graph of "all login failures" or "all events for user_id 123". You can't do that reliably with plain text.
How? Don't build the JSON yourself. Use a library like structlog , which wraps the standard loggingmodule and makes structured logging easy and fast.
Example: `structlog` vs. `logging`
import logging
import structlog
import sys
# --- Standard Logging ---
print("--- Standard Logging ---")
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(message)s",
level=logging.INFO,
stream=sys.stdout,
)
logger = logging.getLogger("standard")
logger.warning("User login failed", extra={"user_id": 456, "ip": "1.2.3.4"})
# --- Structlog (JSON) ---
print("\n--- Structlog ---")
# Configure structlog to output JSON
structlog.configure(
processors=[
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
)
# Get a structlog logger
s_logger = structlog.get_logger("structured")
s_logger.warning("User login failed", user_id=456, ip="1.2.3.4")
Expected Output:
--- Standard Logging ---
2025-11-16 14:30:00,123 - WARNING - User login failed
--- Structlog ---
{"logger": "structured", "level": "warning", "timestamp": "2025-11-16T14:30:00.124Z", "event": "User login failed", "user_id": 456, "ip": "1.2.3.4"}
Notice the standard logger lost the extra context, while thestructlog output is a perfect, machine-readable JSON line.
This is one of the most important questions for a professional developer. The number one rule is:
NEVER, EVER commit secrets to Git.This includes API keys, database passwords, secret keys, or any other credentials. Never hard-code them into your source code.
Analogy: You wouldn't hard-code your house key into your house's blueprints and then post those blueprints on the internet (which is what committing to a public GitHub repo is). A secret is a key , not a blueprint .
Here are the correct ways to handle secrets:
Example: The `.env` Workflow for Development
# File: .gitignore
# THIS IS THE MOST IMPORTANT STEP
venv/
__pycache__/
*.pyc
.env
# ---------------------------------
# File: .env
# This file is NOT committed to Git
DATABASE_URL="postgres://dev_user:dev_pass@localhost/dev_db"
API_KEY="abc123xyz789"
# ---------------------------------
# File: main.py
import os
from dotenv import load_dotenv
# load_dotenv() will find the .env file and load its
# key-value pairs into os.environ
# This is safe to run in production, as it won't
# overwrite existing env vars.
load_dotenv()
# Read secrets from the environment
# os.environ.get() returns None if not found
# os.environ[] will raise a KeyError if not found (better!)
try:
db_url = os.environ["DATABASE_URL"]
api_key = os.environ["API_KEY"]
except KeyError:
print("Error: Environment variables not set!")
# In a real app, you would exit or raise a config error
exit(1)
print(f"Connecting to DB: {db_url[:20]}...")
print(f"Using API Key: {api_key[:3]}...")
Expected Output (when run locally):
Connecting to DB: postgres://dev_user:...
Using API Key: abc...
When this code runs in production, load_dotenv()won't find a .env file, which is fine. It will instead read the DATABASE_URLand API_KEY variables that were injected by your hosting platform.
3 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.