๐ Python Packaging & Environment Config for Services: Let's Learn from ZERO!
When you build a backend service (for example a FastAPI social-feed app), writing Python code is only one part of the job. You also need to know:
- ๐ฆ How to manage packages and dependencies
- ๐ How to create and manage virtual environments
- ๐ How to configure a service without hardcoding values
- ๐๏ธ How to organize a professional backend project
- ๐ How to manage development, testing and production environments
- ๐งญ How to follow the 12-factor app principles
I'll explain what each concept means, why we need it, how it works, and give practical examples. Let's go! ๐
1. ๐ผ๏ธ The Big Picture
Imagine you're building an Instagram-like backend with FastAPI. It needs:
- โก FastAPI to create APIs
- ๐๏ธ SQLAlchemy to talk to PostgreSQL
- โ๏ธ Pydantic to validate data
- ๐ฆ Uvicorn to run the app
- ๐ Alembic for database migrations
- ๐งช pytest to test the app
These are all external Python packages. But then:
- โ How does Python know which packages your project needs?
- โ How do we make sure your teammate's laptop and the production server use compatible versions?
- โ How do we configure database URLs and secret keys without putting them in code?
That's exactly what Python packaging and environment configuration solve.
Your Python Service (FastAPI backend)
โ
โโโ ๐ฆ Packaging โ Poetry / pip-tools โ Dependencies
โโโ ๐ Configuration โ Pydantic Settings โ Environment variables
โโโ ๐๏ธ Project Structure โ app/ ยท tests/ ยท alembic/ ยท pyproject.toml
โ
โผ
โ
Reliable Service: Development โ Testing โ Production
๐ฆ PART 1: Python Packaging
2. ๐ค What is Python packaging?
A package is reusable code written by someone else (or by you) that you can install and use.
| Package | Purpose |
|---|---|
| FastAPI | Build APIs |
| SQLAlchemy | Work with databases |
| Pydantic | Validate data |
| Uvicorn | Run ASGI applications |
| pytest | Test Python code |
| Alembic | Manage database migrations |
| pydantic-settings | Manage application configuration |
Instead of writing all this yourself, you install existing packages:
pip install fastapi
from fastapi import FastAPI
app = FastAPI()
๐ Python packaging is the process of organizing, declaring, managing and distributing your project's dependencies and metadata. In simple words: it tells Python what your project needs and how those requirements should be installed.
3. ๐ What are dependencies?
A dependency is something your project needs in order to work correctly.
from fastapi import FastAPI
from sqlalchemy import create_engine
This project depends on FastAPI and SQLAlchemy. Without them you'd get:
ModuleNotFoundError: No module named 'fastapi'
๐งฉ Two types of dependencies
1๏ธโฃ Direct dependencies: packages your project explicitly uses (fastapi, sqlalchemy, pydantic).
2๏ธโฃ Transitive dependencies: packages your direct dependencies need internally.
Your application
โ
โโโ FastAPI (direct dependency)
โ
โโโ Starlette (transitive dependency)
โ
โโโ other internal dependencies
You never installed Starlette directly, but it arrives because FastAPI needs it.
๐จ Why is dependency management important?
You develop with FastAPI 0.115. Your teammate installs the latest version, which changed something. Now:
- โ Your code works on your laptop
- โ Your teammate's code breaks
- โ The production server behaves differently
That's the dependency/version consistency problem. We solve it with tools like Poetry and pip-tools.
๐ PART 2: Virtual Environments
4. ๐ค What is a virtual environment (venv)?
Two projects, different needs:
| Project | FastAPI | SQLAlchemy |
|---|---|---|
| ๐ ฐ๏ธ Project A | 0.100 | 2.0 |
| ๐ ฑ๏ธ Project B | 0.115 | 2.0 |
If you install everything into one global Python, packages conflict. ๐ฅ
๐ A virtual environment is an isolated Python environment for one project. Think of a separate room ๐ช for each project, each with its own installed packages.
Project A โ Environment A โ FastAPI 0.100, SQLAlchemy 2.0
Project B โ Environment B โ FastAPI 0.115, SQLAlchemy 2.0
๐ ๏ธ How to create one
Open a terminal inside your project folder (social-feed/).
Step 1: create it
python -m venv .venv
| Part | Meaning |
|---|---|
python |
Run Python |
-m |
Run a Python module |
venv |
Use Python's built-in virtual environment module |
.venv |
Name of the environment folder |
Step 2: activate it
Windows PowerShell:
.venv\Scripts\Activate.ps1
Windows Command Prompt:
.venv\Scripts\activate.bat
macOS / Linux:
source .venv/bin/activate
You'll see (.venv) in your prompt. โ
Step 3: install packages
pip install fastapi uvicorn sqlalchemy
Step 4: deactivate
deactivate
๐ฅ What is pip?
pip is Python's package installer:
pip install fastapi # install
pip uninstall fastapi # uninstall
pip list # list installed packages
pip show fastapi # details about one package
๐ง
venvcreates an isolated environment.pipinstalls packages into it. They solve two different problems.
๐งฐ PART 3: Dependency Management Tools
5. ๐ญ What is Poetry?
๐ Poetry is a Python dependency management and packaging tool. Think of it as a project manager for your packages.
It helps you:
- Add and install dependencies
- Manage package versions
- Create and manage virtual environments
- Maintain dependency lock files
- Build and package Python projects
Why Poetry when pip exists? pip installs packages, but managing a whole project's dependencies and reproducible installs takes extra work. Poetry combines several tasks into one workflow.
๐ Create a project with Poetry
Step 1: install Poetry using the official installation guide.
Step 2: create a project
poetry new social-feed
social-feed/
โ
โโโ pyproject.toml
โโโ README.md
โโโ social_feed/
โ โโโ __init__.py
โโโ tests/
โโโ __init__.py
Step 3: add dependencies
poetry add fastapi
poetry add sqlalchemy
poetry add uvicorn
Poetry automatically updates pyproject.toml and the lock file.
Step 4: add development dependencies (needed while developing/testing, not for running the app)
poetry add --group dev pytest
poetry add --group dev ruff
- ๐งช
pytestis for testing - ๐งน
ruffis for linting and code quality
Step 5: install everything
poetry install
Step 6: run commands inside Poetry's environment
poetry run pytest
๐ What is pyproject.toml?
The standard configuration file of modern Python projects. It can contain the project name, version, Python version requirement, dependencies, dev dependencies, build system info, and tool configurations.
[tool.poetry]
name = "social-feed"
version = "1.0.0"
description = "A social media backend"
authors = ["Developer"]
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.115"
sqlalchemy = "^2.0"
uvicorn = "^0.30"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
ruff = "^0.9"
| Configuration | Meaning |
|---|---|
name |
Project name |
version |
Project version |
python |
Supported Python version |
fastapi |
Application dependency |
pytest |
Development dependency |
๐
fastapi = "^0.115"allows compatible versions under Poetry's caret rules. It does not mean exactly0.115is always installed.
๐ What is poetry.lock?
When Poetry resolves your dependencies, it creates poetry.lock, which records the exact resolved versions:
fastapi = 0.115.6
starlette = 0.41.3
pydantic = 2.10.4
(These versions are illustrative.)
If you and your teammate have the same lock file, you both install the same resolved versions instead of independently getting different ones. ๐ฏ
Useful commands:
poetry install # install from project config + lock file
poetry update # update within allowed constraints, refresh lock file
poetry show # display installed/resolved packages
poetry add requests # add a dependency
poetry remove requests # remove a dependency
๐ง
pyproject.toml= the dependencies and constraints you want.poetry.lock= the exact resolution. For an application, commit both to Git.
6. ๐ง What is pip-tools?
๐ pip-tools works alongside
pip. It helps you keep a clean list of direct requirements and generate a fully pinned requirements file.
It provides two commands: pip-compile and pip-sync.
๐ค Why do we need it?
Your project directly needs fastapi, sqlalchemy, uvicorn, but those have their own dependencies. You want a file with the exact versions of everything. pip-compile generates it.
๐ช Step-by-step workflow
Step 1: install
pip install pip-tools
Step 2: create requirements.in (your human-maintained list of direct dependencies)
fastapi
sqlalchemy
uvicorn
Step 3: compile
pip-compile requirements.in
This generates requirements.txt, for example (simplified):
fastapi==0.115.6
sqlalchemy==2.0.36
uvicorn==0.34.0
starlette==0.41.3
The real output also includes transitive dependencies, and can include hashes when configured.
Step 4: sync
pip-sync requirements.txt
| File | Purpose |
|---|---|
requirements.in |
Human-maintained list of direct dependencies |
requirements.txt |
Compiled, pinned dependency list |
-
pip-compilereads your declarations, resolves compatible versions, and produces a pinned file -
pip-syncmakes the active environment match the compiled file by installing missing packages and uninstalling packages not listed
โ๏ธ Poetry vs pip-tools
| Feature | Poetry ๐ญ | pip-tools ๐ง |
|---|---|---|
| Dependency management | Yes | Yes |
| Virtual environments | Built-in support | Uses venv or another tool |
| Dependency lock | poetry.lock |
Compiled requirements.txt
|
| Add dependencies | poetry add |
Edit the .in file |
| Install | poetry install |
pip-sync |
| Build/package support | Yes | Not its main purpose |
| Workflow | Integrated tool | pip-based workflow |
- Poetry: you want an integrated project + dependency workflow
-
pip-tools: your team prefers the traditional
pip+requirements.txtecosystem but wants reliable locking
โ ๏ธ Choose one primary dependency workflow per project. Don't maintain conflicting lock systems.
๐ PART 4: Configuration Management
7. ๐ค What is application configuration?
Your FastAPI backend connects to PostgreSQL:
postgresql://user:password@localhost:5432/social_feed
It also needs a database URL, secret key, API port, debug mode, CORS allowed origins, and session expiry duration. These are configuration values. They tell your app how to behave in a particular environment.
๐ซ Why shouldn't we hardcode configuration?
DATABASE_URL = "postgresql://root:password123@localhost:5432/social_feed"
What's wrong?
- ๐ Security: the password sits in your source code. Push it to GitHub and someone may see it.
- ๐ Environment differences: development uses
localhost:5432/social_feed_dev, production usesproduction-server:5432/social_feed. You don't want to edit source code on every deploy. - ๐ ๏ธ Maintenance: if the password changes, you'd have to modify the code.
โ The solution: environment variables
๐ An environment variable is a named value provided to a running process by its environment.
DATABASE_URL=postgresql://user:password@localhost:5432/social_feed
import os
database_url = os.getenv("DATABASE_URL")
Same code, different configuration
Development Production
DATABASE_URL=localhost DATABASE_URL=cloud-db
DEBUG=true DEBUG=false
โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โผ
Same Python application, no source-code changes โจ
8. โก What is Pydantic Settings?
You know Pydantic validates data:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
User(name="Deepika", age="twenty") raises a validation error. But what if we want to validate our application's configuration?
- Is
DATABASE_URLprovided? - Is
DEBUGa valid boolean? - Is
SESSION_EXPIRE_DAYSa valid integer? - Is the secret key available?
๐ Pydantic Settings is a library built on Pydantic that loads, validates and manages application settings from environment variables and other supported sources.
Step 1: install
pip install pydantic-settings
Step 2: create settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
DEBUG: bool = False
SESSION_EXPIRE_DAYS: int = 7
model_config = SettingsConfigDict(
env_file=".env",
extra="ignore",
)
Line by line:
| Line | Meaning |
|---|---|
BaseSettings |
Base class that lets your class load values from environment variables |
DATABASE_URL: str |
Must be a string. No default, so it's required |
SECRET_KEY: str |
Also required |
DEBUG: bool = False |
Boolean, defaults to False
|
SESSION_EXPIRE_DAYS: int = 7 |
Integer, defaults to 7 |
env_file=".env" |
Also load values from a .env file |
extra="ignore" |
Ignore extra entries that aren't defined as fields |
Step 3: create a .env file in your project root
DATABASE_URL=postgresql://user:password@localhost:5432/social_feed
SECRET_KEY=my-super-secret-key
DEBUG=true
SESSION_EXPIRE_DAYS=7
Step 4: create a settings object
settings = Settings()
What happens:
- Pydantic Settings looks for the values
- It reads environment variables and the configured
.envfile - It converts values to the required types where valid
- It validates required fields
- It creates the settings object
print(settings.DATABASE_URL)
print(settings.DEBUG) # True (converted from "true")
print(settings.SESSION_EXPIRE_DAYS) # 7 (converted from "7")
๐ฅ What if a required value is missing?
Remove SECRET_KEY and run Settings(). Pydantic raises a validation error, because SECRET_KEY is required and has no default.
That's good! ๐ Instead of starting with incomplete configuration, the app fails early and tells you what's missing. This is called fail-fast configuration validation.
Step 5: use settings across your app
app/config.py:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
DEBUG: bool = False
model_config = SettingsConfigDict(
env_file=".env",
extra="ignore",
)
settings = Settings()
In another file:
from app.config import settings
print(settings.DATABASE_URL)
Now every part of your app reads from one centralized configuration.
๐ The configuration flow
.env file / environment variables
โ DATABASE_URL, SECRET_KEY, DEBUG
โผ
Pydantic Settings โ reads, converts and validates values
โผ
Settings object โ settings.DATABASE_URL
โผ
Application components โ database, security, API, services
โ๏ธ Environment variable vs .env file
| Environment variable |
.env file |
|---|---|
| A variable supplied to a running process | A text file containing key-value pairs |
| Can be set by the OS, shell or hosting platform | Commonly used for local development |
| Exists in the process environment | Must be loaded by an app or library |
| Doesn't require a physical file | Is a physical file |
๐ Pydantic Settings gives environment variables higher priority than
.envvalues. So a production environment variable can override a local file value.
๐ก๏ธ Security rule
๐จ Never commit your real
.envfile containing passwords and secrets to GitHub.
.gitignore:
.env
.venv/
__pycache__/
.pytest_cache/
Instead, commit a .env.example so teammates know which settings they need, without exposing secrets:
DATABASE_URL=
SECRET_KEY=
DEBUG=false
SESSION_EXPIRE_DAYS=7
For production, use your hosting provider's environment-variable or secret-management facility.
๐๏ธ PART 5: Standard Python Service Project Layout
9. ๐ค What is project structure?
Imagine your whole backend lives in one main.py: routes, database connections, authentication, business logic, queries, configuration, even tests. It works at first, but as the project grows the file becomes huge and unmaintainable. ๐ต
Professional apps separate responsibilities into folders and files. That's a project layout.
10. ๐๏ธ Recommended FastAPI service structure
social-feed/
โ
โโโ app/ # Main application code
โ โโโ api/ # API routes and endpoints
โ โโโ models/ # Database models
โ โโโ schemas/ # Request/response validation
โ โโโ services/ # Business logic
โ โโโ repositories/ # Database access logic
โ โโโ common/ # Shared utilities and exceptions
โ โโโ config.py # Application configuration
โ โโโ database.py # Database connection
โ โโโ main.py # FastAPI entry point
โ
โโโ tests/ # Automated tests
โ โโโ test_auth.py
โ โโโ test_feed.py
โ โโโ test_users.py
โ
โโโ alembic/ # Database migrations
โ โโโ versions/
โ โโโ env.py
โ โโโ script.py.mako
โ
โโโ pyproject.toml # Project and tool configuration
โโโ poetry.lock # Locked dependency versions
โโโ alembic.ini # Alembic configuration
โโโ README.md # Project documentation
โโโ .env # Local configuration (NOT committed)
โโโ .gitignore # Files Git should ignore
Let's understand each part. ๐
10.1 ๐ app/: the main application folder
Where your actual backend code lives (main.py, config.py, database.py, and the sub-folders).
10.2 ๐ฃ๏ธ api/: API routes
An API route defines how your backend responds to HTTP requests:
@router.get("/users")
def get_users():
return {"message": "All users"}
-
@router.getdefines a GET endpoint -
/usersis the endpoint path - The function runs when a matching request arrives
api/
routes/
auth.py
users.py
posts.py
feed.py
Each file handles a group of endpoints.
10.3 ๐๏ธ models/: database models
A model describes a database table using Python classes (with an ORM like SQLAlchemy):
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String)
email = Column(String)
๐ง Models = how data is stored in the database.
10.4 โ๏ธ schemas/: data validation
Schemas define the structure of incoming and outgoing data:
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str
email: str
A registration request might contain:
{
"username": "deepika",
"email": "deepika@example.com"
}
๐ง Models = database structure. Schemas = API data structure and validation. They are not the same thing!
10.5 ๐ง services/: business logic
Where the application rules live. Imagine a user wants to follow another user. The service checks:
- Does the target user exist?
- Is the user trying to follow themselves?
- Does the follow relationship already exist?
- If all valid, create the follow relationship
def follow_user(current_user, target_user):
if current_user.id == target_user.id:
raise ValueError("Cannot follow yourself")
# Follow business logic goes here
๐ง Services = what the application should do.
10.6 ๐ repositories/: database access
A repository is the layer that talks to the database:
def get_user_by_id(db, user_id):
return db.query(User).filter(User.id == user_id).first()
๐ง Repository = how we retrieve or store data. Service = why and when we do it. This separation makes code easier to test and maintain.
10.7 ๐งฐ common/: shared utilities
Reusable code used in many places:
common/
exceptions.py
security.py
responses.py
pagination.py
10.8 โ๏ธ config.py: configuration
Defines and loads application settings. Other modules import the settings object instead of reading environment variables independently.
10.9 ๐ database.py: database connection
Typically contains the engine, the session factory, session management and the ORM base class:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(
bind=engine,
autoflush=False,
)
10.10 ๐ช main.py: application entry point
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "healthy"}
Start it with:
uvicorn app.main:app --reload
-
uvicorn: the ASGI server -
app.main: themain.pymodule inside theapppackage -
app: the FastAPI application object -
--reload: auto-reload when code changes (development)
10.11 ๐งช tests/: testing
def test_addition():
assert 2 + 3 == 5
For a backend: test_auth.py, test_posts.py, test_feed.py, test_database.py. Run them with:
pytest
10.12 ๐ alembic/: database migrations
A database migration is a controlled change to the database schema. Say your users table has id, username, email, and later you want to add profile_picture. Instead of changing the database manually:
alembic revision --autogenerate -m "add profile picture"
alembic upgrade head
๐ง Alembic = version control for database schema changes.
11. ๐ How all the folders work together
A user sends GET /posts/101:
1. ๐ Client sends HTTP request GET /posts/101
โผ
2. ๐ฃ๏ธ API layer receives the request, identifies the endpoint
โผ
3. ๐ง Service layer applies business rules
โผ
4. ๐ Repository layer fetches the post from the database
โผ
5. ๐๏ธ Database returns post information
โผ
6. โ๏ธ Schema / API response formats and validates the response
{
"id": 101,
"caption": "Beautiful day!",
"username": "deepika"
}
Each folder has one specific responsibility. That's why professional projects are structured this way. ๐ฏ
๐ PART 6: Config as Code
12. ๐ค What is Config as Code?
๐ Config as Code means defining and managing application configuration through structured, version-controlled code or configuration files, rather than manually changing settings in different places.
Instead of scattering this through your code:
if environment == "production":
database = "production_db"
else:
database = "development_db"
centralize it:
class Settings(BaseSettings):
DATABASE_URL: str
DEBUG: bool = False
SECRET_KEY: str
โ Why is this useful?
- ๐ค Consistency: everyone follows the same configuration structure
- ๐ ๏ธ Maintainability: change configuration without editing many files
- ๐ Environment separation: development, testing and production can use different settings
- โ๏ธ Validation: Pydantic checks the values
- ๐ Documentation: the settings class tells developers what the app expects
โ๏ธ Config as Code vs hardcoding
| Hardcoding | Config as Code |
|---|---|
| Values written directly in application logic | Values are centralized |
| Changing values may require source changes | Values can come from the environment |
| Secrets may accidentally enter Git | Secrets can be supplied externally |
| Harder to manage environments | Easier to manage environments |
โ ๏ธ Important: Config as Code does NOT mean putting every secret into a Python file and pushing it to GitHub. It means managing the configuration structure systematically while supplying sensitive values securely from the environment or a secret manager.
๐งญ PART 7: 12-Factor App Principles
13. ๐ค What is a 12-Factor App?
๐ The 12-Factor App is a methodology for building modern, scalable, maintainable software-as-a-service applications that run reliably across different environments and deployment platforms.
You don't need to memorize all 12 immediately. This topic focuses on Codebase, Dependencies, Config, but here's the complete picture:
| # | Factor | Principle |
|---|---|---|
| 1 | ๐ Codebase | One codebase tracked in version control, with multiple deployments |
| 2 | ๐ฆ Dependencies | Explicitly declare and isolate dependencies |
| 3 | โ๏ธ Config | Store configuration in the environment |
| 4 | ๐ Backing services | Treat databases, caches and other services as attached resources |
| 5 | ๐๏ธ Build, release, run | Separate building, releasing and running |
| 6 | ๐ง Processes | Run the app as one or more stateless processes |
| 7 | ๐ช Port binding | Export services through port binding |
| 8 | ๐ Concurrency | Scale out using processes |
| 9 | โป๏ธ Disposability | Fast startup and graceful shutdown |
| 10 | ๐ช Dev/prod parity | Keep development, staging and production as similar as possible |
| 11 | ๐ Logs | Treat logs as event streams |
| 12 | ๐ ๏ธ Admin processes | Run administrative tasks as one-off processes |
Let's go through them. ๐
14. ๐ Factor 1: Codebase
One codebase tracked in version control, with multiple deployments.
You have one backend project in GitHub (SocialFeed/) and deploy the same codebase to Development, Staging and Production. You don't need three separate codebases. You maintain one and configure it differently:
GitHub Repository
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โผ โผ โผ
Development Staging Production
Development โ development database
Staging โ staging database
Production โ production database
Why? Separate codebases slowly drift apart. A bug fix might reach production but get forgotten in development. One source of truth in Git makes collaboration easier.
15. ๐ฆ Factor 2: Dependencies
Explicitly declare and isolate dependencies.
Explicitly declare: clearly specify the packages your project requires:
[project]
dependencies = [
"fastapi",
"sqlalchemy",
"pydantic-settings",
]
Isolate: keep dependencies separate from other projects:
python -m venv .venv
Why? Imagine your project works only because you manually installed a package on your laptop and forgot to list it in requirements.txt or pyproject.toml. Your teammate clones the project and it crashes. That's the undeclared dependency problem. Never rely on packages being installed accidentally or globally.
16. โ๏ธ Factor 3: Config
Store configuration in the environment.
Development: DATABASE_URL=localhost, DEBUG=true. Production: DATABASE_URL=production-database, DEBUG=false. The same code runs in both; only the supplied configuration differs.
What counts as configuration?
- Database URLs
- API keys and secret keys
- External service URLs
- Environment-specific feature flags
- Credentials
- Runtime settings
What should NOT be configuration? Application code and internal business logic. A function that calculates a user's feed ranking is business logic, not configuration.
17. ๐ Factor 4: Backing Services
A backing service is an external service your app depends on: PostgreSQL, Redis, an email service, cloud storage, external payment APIs.
Your app shouldn't care whether PostgreSQL runs locally or on a cloud server, as long as it can connect using the configured details:
DATABASE_URL=postgresql://localhost/social_feed
Later you can switch to a hosted PostgreSQL URL without rewriting your database logic. Treat external services as attached resources that can be configured.
18. ๐๏ธ Factor 5: Build, Release, Run
Deployment is separated into three stages:
- ๐จ Build: convert source code into a deployable artifact, including dependencies
- ๐ฆ Release: combine the build with the configuration for a particular deployment
- โถ๏ธ Run: start the application from that release
Example: Build = install dependencies and prepare the app. Release = provide the production database URL and secrets. Run = start the FastAPI server.
Why separate them? So you can deploy the same built application with different configurations without rebuilding everything.
19. ๐ง Factor 6: Processes
Execute the app as one or more stateless processes.
A process is a running instance of a program, for example what starts when you run uvicorn app.main:app.
Stateless means the process shouldn't rely on its local memory or filesystem as the permanent storage of important user data or session state.
- โ Poor: store a user's session only in one process's memory. If the process restarts, the session disappears.
- โ Better: store sessions in a shared database or suitable external storage.
This matters especially when multiple app instances run. Important persistent data belongs in backing services, not just inside a process's memory.
20. ๐ช Factor 7: Port Binding
The app makes itself available through a network port:
uvicorn app.main:app --host 0.0.0.0 --port 8000
-
0.0.0.0โ listen on all available network interfaces -
8000โ the port
Your app is then reachable at http://localhost:8000. The service should be self-contained and expose itself through a port.
21. ๐ Factor 8: Concurrency
100 users hit your app at once. You can scale using multiple processes or instances:
Incoming requests
โ
โโโโโโโโโโโผโโโโโโโโโโ
โผ โผ โผ
App 1 App 2 App 3
โโโโโโโโโโโผโโโโโโโโโโ
โผ
PostgreSQL
A load balancer distributes requests among instances. This is horizontal scaling: instead of making one server bigger, you add more instances. Scale by running more processes.
22. โป๏ธ Factor 9: Disposability
Processes should start quickly and shut down gracefully. When your platform restarts your backend, a good app should:
- Start without unnecessary delays
- Accept requests once ready
- Stop accepting new requests when shutting down
- Finish ongoing work where possible
- Close database connections cleanly
FastAPI supports lifespan events:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Application starting")
yield
print("Application shutting down")
app = FastAPI(lifespan=lifespan)
Code before yield runs on startup, and code after it runs on shutdown. In a real service, you'd initialize and clean up shared resources here.
23. ๐ช Factor 10: Dev/Prod Parity
Keep development, staging and production as similar as practical. Imagine:
Development: Python 3.12, PostgreSQL 16, FastAPI
Production: Python 3.10, MySQL, different dependency versions
It works in development but fails in production. Why? The environments are too different. 12-factor encourages minimizing gaps in three areas:
- โฑ๏ธ Time: reduce the time between writing code and deploying it
- ๐ฅ Personnel: developers stay involved in deployment
- ๐งฐ Tools: use similar technology across environments
24. ๐ Factor 11: Logs
A log is a record of something that happened inside an application:
INFO: Application started
INFO: User logged in
WARNING: Database response was slow
ERROR: Failed to create post
12-factor says: treat logs as event streams. The app writes logs to stdout (and errors to stderr) instead of depending on a particular log-file location. The hosting platform collects and stores them.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Application started")
logger.error("Unable to connect to database")
Your app generates logs; the execution environment handles their collection and storage.
25. ๐ ๏ธ Factor 12: Admin Processes
Admin processes are one-time tasks that help maintain or operate an app:
- Database migrations
- Creating an administrator account
- Importing initial data
- Running maintenance scripts
- Cleaning old records
alembic upgrade head # apply database migrations
python scripts/create_admin.py # create an administrator account
These aren't normal HTTP request-handling operations. The principle: run them using the same codebase, configuration and environment as the app.
๐๏ธ PART 8: Putting Everything Together
26. ๐งต Setting up a clean FastAPI service
Your mentor says: "Create a clean Python service with dependency management and environment-based configuration." Here's how:
Step 1: create the project
mkdir social-feed
cd social-feed
Step 2: create and activate a virtual environment
python -m venv .venv
.venv\Scripts\Activate.ps1 # Windows PowerShell
# source .venv/bin/activate # macOS / Linux
Step 3: install dependencies
pip install fastapi uvicorn sqlalchemy pydantic-settings
pip install pytest ruff # development tools
Step 4: save dependencies
pip freeze > requirements.txt
This records the installed packages and versions. For a more controlled workflow, use requirements.in + pip-compile instead.
Step 5: create the project structure
social-feed/
โ
โโโ app/
โ โโโ api/
โ โโโ models/
โ โโโ schemas/
โ โโโ services/
โ โโโ repositories/
โ โโโ common/
โ โโโ config.py
โ โโโ database.py
โ โโโ main.py
โ
โโโ tests/
โ โโโ test_health.py
โ
โโโ alembic/
โ โโโ versions/
โ
โโโ .env
โโโ .env.example
โโโ .gitignore
โโโ requirements.txt
โโโ README.md
Step 6: add configuration (app/config.py)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
DEBUG: bool = False
model_config = SettingsConfigDict(
env_file=".env",
extra="ignore",
)
settings = Settings()
Step 7: create your .env
DATABASE_URL=postgresql://user:password@localhost:5432/social_feed
SECRET_KEY=local-development-secret
DEBUG=true
Step 8: create the FastAPI app (app/main.py)
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "healthy"}
Step 9: run it
uvicorn app.main:app --reload
Open http://localhost:8000/docs to see the Swagger documentation. ๐
Step 10: test it (tests/test_health.py)
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
pytest
๐ You've connected packaging, virtual environments, configuration, project structure and testing in one small service.
๐ This is a learning skeleton. A production service also needs proper database initialization, secret management, logging, migration setup and deployment configuration.
โ๏ธ PART 9: Differences You Must Never Confuse
| Concept | What it does |
|---|---|
venv |
Creates an isolated Python environment |
pip |
Installs Python packages |
| Poetry | Manages dependencies, environments and packaging |
| pip-tools | Compiles and synchronizes pinned dependencies |
pyproject.toml |
Stores project metadata and tool configuration |
poetry.lock |
Records resolved dependency versions |
requirements.txt |
Lists pinned packages for installation |
| Pydantic | Validates data |
| Pydantic Settings | Loads and validates application configuration |
.env |
Stores local environment configuration values |
config.py |
Defines and exposes application settings |
alembic/ |
Manages database schema migrations |
tests/ |
Contains automated tests |
โ ๏ธ PART 10: Common Beginner Mistakes
- โ Installing packages globally. Always use the project's environment.
- โ Not maintaining dependency files. Your teammate should be able to recreate the environment.
- โ Committing
.envto GitHub. Never expose real passwords or secret keys. - โ Hardcoding database URLs. Use environment-based configuration.
- โ Putting everything in
main.py. Separate routes, services, repositories and models. - โ Confusing models and schemas. Database representation and API validation are different.
- โ Using different package versions everywhere. Maintain consistent dependency resolution.
- โ Ignoring production configuration. Production secrets should come from secure deployment configuration, not development files.
๐งช PART 11: Test Your Understanding!
Try these before peeking at the answers! โค๏ธ
-
Which tool creates an isolated Python environment?
- A)
pip - B)
venv - C) Pydantic
- D) Alembic
- A)
-
What is the primary purpose of
poetry.lock?- A) Store database passwords
- B) Store API routes
- C) Record resolved dependency versions
- D) Store test cases
-
Which library helps load and validate environment-based application settings?
- A) pytest
- B) SQLAlchemy
- C) Pydantic Settings
- D) Uvicorn
-
Where should business logic generally live in a layered FastAPI project?
- A)
services/ - B)
tests/ - C)
alembic/ - D)
schemas/
- A)
-
Which 12-factor principle says configuration should be stored in the environment?
- A) Codebase
- B) Config
- C) Processes
- D) Logs
-
What should you do with a real
.envfile containing secrets?- A) Push it to GitHub
- B) Put it in
tests/ - C) Add it to
.gitignore - D) Rename it
main.py
๐ Click to reveal answers
venv creates isolated environments (pip just installs packages)poetry.lock records exact resolved dependency versionsservices/ holds business logic.env to .gitignore and commit a .env.example instead
๐ค PART 12: How to Explain This to Your Mentor
Python packaging and environment configuration help us build maintainable and reproducible backend services. We use dependency management tools like Poetry or pip-tools to declare and lock package versions, so the same dependencies can be installed across development and production. We use virtual environments to isolate project dependencies and avoid conflicts between projects. For configuration, we use Pydantic Settings to load and validate environment variables such as database URLs, secret keys and debug settings, instead of hardcoding them. We organize the service into modules such as
app,testsandalembic, with clear responsibilities for API routes, business logic, database access, testing and migrations. Finally, we follow the 12-factor methodology, particularly explicitly declaring dependencies and keeping environment-specific configuration outside the codebase, which makes the service easier to deploy, maintain and scale.
๐ Five Key Takeaways
- ๐ฆ Packaging: manage the packages your project needs
- ๐ Virtual environments: isolate dependencies for each project
- ๐ญ Poetry / pip-tools: maintain consistent, reproducible dependencies
- โ๏ธ Pydantic Settings: read and validate environment-based configuration
- ๐งญ 12-factor principles: build services that are portable, maintainable and deployment-friendly
๐ What's Next?
Don't just memorize the terms. Build one small FastAPI service with a .venv, pyproject.toml, config.py, .env and a tests/ folder. Once you've done it yourself, these concepts become much easier to explain. ๐ก
If this post helped you, drop a โค๏ธ or ๐ฆ and share it with a friend learning Python backend development. Happy coding! ๐ฉโ๐ปโจ
Top comments (0)