AdvancedPython · Lesson 8 of 9

Project Structure, Quality Tools & Deployment

pyproject.toml, src layout, ruff, logging, configuration and Docker.

A modern Python project keeps its code under src/<package>/, tests under tests/, and all metadata, dependencies and tool settings in one pyproject.toml. Tools like uv or pip install from it; pip install -e . installs your package in editable mode during development.

Automate quality: ruff (linting and formatting — very fast), mypy (types) and pytest (tests). Run them locally and in CI on every push.

In production use the logging module (not print), read configuration from environment variables, run the API with a production server (FastAPI uses Uvicorn), and package everything in a small Docker image that runs as a non-root user.

Project layoutText
results-api/
├── pyproject.toml
├── Dockerfile
├── src/
│   └── results_api/
│       ├── __init__.py
│       ├── main.py
│       └── db.py
└── tests/
    └── test_main.py
pyproject.tomlTOML
[project]
name = "results-api"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "fastapi[standard]>=0.115",
  "psycopg[binary,pool]>=3.2",
]

[project.optional-dependencies]
dev = ["pytest>=8", "mypy>=1.11", "ruff>=0.6"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]

[tool.mypy]
strict = true

[tool.pytest.ini_options]
testpaths = ["tests"]
src/results_api/logging_setup.pyPython
import logging
import os


def configure_logging() -> logging.Logger:
    logging.basicConfig(
        level=os.environ.get("LOG_LEVEL", "INFO"),
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
    )
    return logging.getLogger("results_api")


log = configure_logging()
log.info("service starting", extra={"port": os.environ.get("PORT", "8000")})
Runs in your browser · Python
DockerfileDockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY pyproject.toml ./
COPY src ./src
RUN pip install --no-cache-dir .
USER nobody
EXPOSE 8000
CMD ["uvicorn", "results_api.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
TerminalShell
pip install -e ".[dev]"
ruff check . && ruff format --check . && mypy src && pytest
docker build -t results-api . && docker run -p 8000:8000 -e DATABASE_URL=... results-api

Key points

  • One pyproject.toml holds dependencies and tool settings; use the src layout.
  • ruff + mypy + pytest on every push keeps quality high.
  • Log with logging, configure with env vars, run as non-root in Docker.

Exercise

Restructure your FastAPI + PostgreSQL project into the layout above, add the quality tools, make ruff, mypy and pytest pass, then build and run the Docker image locally.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Move the code into src/results_api/, the tests into tests/, and declare everything in pyproject.toml (as in the lesson). pip install -e ".[dev]" installs the package in editable mode, so tests import it as results_api. Then the three quality gates must all pass before building the Docker image.

src/results_api/grading.pyPython
def grade_for(score: int) -> str:
    """Return the NECTA-style grade letter for a score from 0 to 100."""
    if not 0 <= score <= 100:
        raise ValueError("score must be between 0 and 100")
    for minimum, grade in ((75, "A"), (65, "B"), (45, "C"), (30, "D")):
        if score >= minimum:
            return grade
    return "F"
tests/test_grading.pyPython
import pytest

from results_api.grading import grade_for


@pytest.mark.parametrize(("score", "grade"), [(75, "A"), (74, "B"), (30, "D"), (29, "F")])
def test_grade_for(score: int, grade: str) -> None:
    assert grade_for(score) == grade


def test_rejects_out_of_range() -> None:
    with pytest.raises(ValueError):
        grade_for(101)
TerminalShell
pip install -e ".[dev]"
ruff check . && ruff format --check . && mypy src && pytest -q
# All checks passed! ... Success: no issues found ... 5 passed

docker build -t results-api .
docker run -p 8000:8000 -e DATABASE_URL="postgresql://..." results-api

Check your understanding

  1. What belongs in pyproject.toml?

  2. What does pip install -e . do?

  3. Which tool lints and formats Python code very quickly?

  4. Why use the logging module instead of print in production?

Ask AI