DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Python Packaging & Environment Config for Services: venv, Poetry, pip-tools, Pydantic Settings & 12-Factor Explained ๐Ÿ

๐Ÿ 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
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฆ 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
Enter fullscreen mode Exit fullscreen mode
from fastapi import FastAPI

app = FastAPI()
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ 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
Enter fullscreen mode Exit fullscreen mode

This project depends on FastAPI and SQLAlchemy. Without them you'd get:

ModuleNotFoundError: No module named 'fastapi'
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› ๏ธ How to create one

Open a terminal inside your project folder (social-feed/).

Step 1: create it

python -m venv .venv
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Windows Command Prompt:

.venv\Scripts\activate.bat
Enter fullscreen mode Exit fullscreen mode

macOS / Linux:

source .venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

You'll see (.venv) in your prompt. โœ…

Step 3: install packages

pip install fastapi uvicorn sqlalchemy
Enter fullscreen mode Exit fullscreen mode

Step 4: deactivate

deactivate
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฅ 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
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  venv creates an isolated environment. pip installs 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:

  1. Add and install dependencies
  2. Manage package versions
  3. Create and manage virtual environments
  4. Maintain dependency lock files
  5. 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
Enter fullscreen mode Exit fullscreen mode
social-feed/
โ”‚
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ social_feed/
โ”‚   โ””โ”€โ”€ __init__.py
โ””โ”€โ”€ tests/
    โ””โ”€โ”€ __init__.py
Enter fullscreen mode Exit fullscreen mode

Step 3: add dependencies

poetry add fastapi
poetry add sqlalchemy
poetry add uvicorn
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • ๐Ÿงช pytest is for testing
  • ๐Ÿงน ruff is for linting and code quality

Step 5: install everything

poetry install
Enter fullscreen mode Exit fullscreen mode

Step 6: run commands inside Poetry's environment

poetry run pytest
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ 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"
Enter fullscreen mode Exit fullscreen mode
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 exactly 0.115 is 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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode

Step 2: create requirements.in (your human-maintained list of direct dependencies)

fastapi
sqlalchemy
uvicorn
Enter fullscreen mode Exit fullscreen mode

Step 3: compile

pip-compile requirements.in
Enter fullscreen mode Exit fullscreen mode

This generates requirements.txt, for example (simplified):

fastapi==0.115.6
sqlalchemy==2.0.36
uvicorn==0.34.0
starlette==0.41.3
Enter fullscreen mode Exit fullscreen mode

The real output also includes transitive dependencies, and can include hashes when configured.

Step 4: sync

pip-sync requirements.txt
Enter fullscreen mode Exit fullscreen mode
File Purpose
requirements.in Human-maintained list of direct dependencies
requirements.txt Compiled, pinned dependency list
  • pip-compile reads your declarations, resolves compatible versions, and produces a pinned file
  • pip-sync makes 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.txt ecosystem 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

What's wrong?

  1. ๐Ÿ”“ Security: the password sits in your source code. Push it to GitHub and someone may see it.
  2. ๐ŸŒ Environment differences: development uses localhost:5432/social_feed_dev, production uses production-server:5432/social_feed. You don't want to edit source code on every deploy.
  3. ๐Ÿ› ๏ธ 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
Enter fullscreen mode Exit fullscreen mode
import os

database_url = os.getenv("DATABASE_URL")
Enter fullscreen mode Exit fullscreen mode
Same code, different configuration

Development                 Production
DATABASE_URL=localhost      DATABASE_URL=cloud-db
DEBUG=true                  DEBUG=false
        โ”‚                           โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ–ผ
     Same Python application, no source-code changes โœจ
Enter fullscreen mode Exit fullscreen mode

8. โšก What is Pydantic Settings?

You know Pydantic validates data:

from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int
Enter fullscreen mode Exit fullscreen mode

User(name="Deepika", age="twenty") raises a validation error. But what if we want to validate our application's configuration?

  • Is DATABASE_URL provided?
  • Is DEBUG a valid boolean?
  • Is SESSION_EXPIRE_DAYS a 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
Enter fullscreen mode Exit fullscreen mode

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",
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4: create a settings object

settings = Settings()
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Pydantic Settings looks for the values
  2. It reads environment variables and the configured .env file
  3. It converts values to the required types where valid
  4. It validates required fields
  5. 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")
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ฅ 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()
Enter fullscreen mode Exit fullscreen mode

In another file:

from app.config import settings

print(settings.DATABASE_URL)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

โš–๏ธ 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 .env values. So a production environment variable can override a local file value.

๐Ÿ›ก๏ธ Security rule

๐Ÿšจ Never commit your real .env file containing passwords and secrets to GitHub.

.gitignore:

.env
.venv/
__pycache__/
.pytest_cache/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode
  • @router.get defines a GET endpoint
  • /users is the endpoint path
  • The function runs when a matching request arrives
api/
    routes/
        auth.py
        users.py
        posts.py
        feed.py
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode

A registration request might contain:

{
  "username": "deepika",
  "email": "deepika@example.com"
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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:

  1. Does the target user exist?
  2. Is the user trying to follow themselves?
  3. Does the follow relationship already exist?
  4. 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
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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()
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

10.10 ๐Ÿšช main.py: application entry point

from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health_check():
    return {"status": "healthy"}
Enter fullscreen mode Exit fullscreen mode

Start it with:

uvicorn app.main:app --reload
Enter fullscreen mode Exit fullscreen mode
  • uvicorn: the ASGI server
  • app.main: the main.py module inside the app package
  • app: the FastAPI application object
  • --reload: auto-reload when code changes (development)

10.11 ๐Ÿงช tests/: testing

def test_addition():
    assert 2 + 3 == 5
Enter fullscreen mode Exit fullscreen mode

For a backend: test_auth.py, test_posts.py, test_feed.py, test_database.py. Run them with:

pytest
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode
{
  "id": 101,
  "caption": "Beautiful day!",
  "username": "deepika"
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

centralize it:

class Settings(BaseSettings):
    DATABASE_URL: str
    DEBUG: bool = False
    SECRET_KEY: str
Enter fullscreen mode Exit fullscreen mode

โœ… Why is this useful?

  1. ๐Ÿค Consistency: everyone follows the same configuration structure
  2. ๐Ÿ› ๏ธ Maintainability: change configuration without editing many files
  3. ๐ŸŒ Environment separation: development, testing and production can use different settings
  4. โœ”๏ธ Validation: Pydantic checks the values
  5. ๐Ÿ“– 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
Enter fullscreen mode Exit fullscreen mode
Development โ†’ development database
Staging     โ†’ staging database
Production  โ†’ production database
Enter fullscreen mode Exit fullscreen mode

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",
]
Enter fullscreen mode Exit fullscreen mode

Isolate: keep dependencies separate from other projects:

python -m venv .venv
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. ๐Ÿ”จ Build: convert source code into a deployable artifact, including dependencies
  2. ๐Ÿ“ฆ Release: combine the build with the configuration for a particular deployment
  3. โ–ถ๏ธ 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2: create and activate a virtual environment

python -m venv .venv
.venv\Scripts\Activate.ps1      # Windows PowerShell
# source .venv/bin/activate     # macOS / Linux
Enter fullscreen mode Exit fullscreen mode

Step 3: install dependencies

pip install fastapi uvicorn sqlalchemy pydantic-settings
pip install pytest ruff          # development tools
Enter fullscreen mode Exit fullscreen mode

Step 4: save dependencies

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Step 7: create your .env

DATABASE_URL=postgresql://user:password@localhost:5432/social_feed
SECRET_KEY=local-development-secret
DEBUG=true
Enter fullscreen mode Exit fullscreen mode

Step 8: create the FastAPI app (app/main.py)

from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health_check():
    return {"status": "healthy"}
Enter fullscreen mode Exit fullscreen mode

Step 9: run it

uvicorn app.main:app --reload
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode
pytest
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ‰ 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

  1. โŒ Installing packages globally. Always use the project's environment.
  2. โŒ Not maintaining dependency files. Your teammate should be able to recreate the environment.
  3. โŒ Committing .env to GitHub. Never expose real passwords or secret keys.
  4. โŒ Hardcoding database URLs. Use environment-based configuration.
  5. โŒ Putting everything in main.py. Separate routes, services, repositories and models.
  6. โŒ Confusing models and schemas. Database representation and API validation are different.
  7. โŒ Using different package versions everywhere. Maintain consistent dependency resolution.
  8. โŒ 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! โค๏ธ

  1. Which tool creates an isolated Python environment?

    • A) pip
    • B) venv
    • C) Pydantic
    • D) Alembic
  2. 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
  3. Which library helps load and validate environment-based application settings?

    • A) pytest
    • B) SQLAlchemy
    • C) Pydantic Settings
    • D) Uvicorn
  4. Where should business logic generally live in a layered FastAPI project?

    • A) services/
    • B) tests/
    • C) alembic/
    • D) schemas/
  5. Which 12-factor principle says configuration should be stored in the environment?

    • A) Codebase
    • B) Config
    • C) Processes
    • D) Logs
  6. What should you do with a real .env file 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
  1. B: venv creates isolated environments (pip just installs packages)
  2. C: poetry.lock records exact resolved dependency versions
  3. C: Pydantic Settings loads and validates configuration
  4. A: services/ holds business logic
  5. B: Factor 3, Config
  6. C: Add .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, tests and alembic, 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

  1. ๐Ÿ“ฆ Packaging: manage the packages your project needs
  2. ๐Ÿ  Virtual environments: isolate dependencies for each project
  3. ๐ŸŽญ Poetry / pip-tools: maintain consistent, reproducible dependencies
  4. โš™๏ธ Pydantic Settings: read and validate environment-based configuration
  5. ๐Ÿงญ 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)