--- title: Python language-specific guide linkTitle: Python description: Containerize Python apps using Docker keywords: Docker, getting started, Python, language summary: | This guide explains how to containerize Python applications using Docker. aliases: - /language/python/ - /guides/language/python/ - /language/python/build-images/ - /language/python/run-containers/ - /language/python/containerize/ - /language/python/develop/ - /language/python/configure-ci-cd/ - /guides/language/python/configure-ci-cd/ - /language/python/deploy/ - /guides/python/configure-github-actions/ - /guides/python/containerize/ - /guides/python/deploy/ - /guides/python/develop/ - /guides/python/lint-format-typing/ - /guides/python/secure-supply-chain/ params: tags: [languages] time: 20 minutes --- > **Acknowledgment** > > This guide is a community contribution. Docker would like to thank > [Esteban Maya](https://www.linkedin.com/in/esteban-x64/) and [Igor Aleksandrov](https://www.linkedin.com/in/igor-aleksandrov/) for their contribution > to this guide. The Python language-specific guide teaches you how to containerize a Python application using Docker. In this guide, you’ll learn how to: - Containerize and run a Python application - Set up a local environment to develop a Python application using containers - Lint, format, typing and best practices Start by containerizing an existing Python application. ## Containerize a Python application ### Prerequisites - You have installed the latest version of [Docker Desktop](/get-started/get-docker.md). ### Overview Containerizing your application means packaging it together with its dependencies, configuration, and runtime into a single portable unit called a container image. Running that image creates a container, an isolated process that behaves the same on any machine, whether it's your laptop, a CI runner, or a production server. In this section, you'll containerize a simple [FastAPI](https://fastapi.tiangolo.com) web application. You'll write a `Dockerfile` that describes how to build the image, add a `compose.yaml` file that defines how Docker runs your container, and then build and start the application with one command. You'll use [Docker Hardened Images](/dhi/) as the base. These are minimal, secure Python images maintained by Docker. ### Create the application The sample application is a minimal FastAPI service with a single endpoint that returns a JSON greeting. Create the following files in a new `python-docker-example` directory. To create all the files at once, switch to the **Scaffold script** tab in the file browser and copy the shell command. {{< files name="python-docker-example" >}} {{< file path="app.py" status="new" >}} ```python # A minimal FastAPI application. # The root endpoint (GET /) returns a JSON "Hello World" response. # See https://fastapi.tiangolo.com/ for the framework reference. from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"} ``` {{< /file >}} {{< file path="requirements.txt" status="new" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 uvicorn==0.34.3 ``` {{< /file >}} {{< file path=".gitignore" status="new" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} If you already have Python installed and want to verify the app works before containerizing it, you can run it locally: ```console $ python3 -m venv .venv $ source .venv/bin/activate $ pip install -r requirements.txt $ uvicorn app:app --reload ``` > [!NOTE] > > On Windows, activate the virtual environment with `.venv\Scripts\activate` > instead of `source .venv/bin/activate`. If you don't have Python installed, skip ahead to the next section. The remaining steps run the application in a container, with no local Python required. ### Create the Docker assets Sign in to the DHI registry so Docker can pull the Python base images during the build. The available Python images are listed in the [catalog](https://hub.docker.com/hardened-images/catalog/dhi/python). ```console $ docker login dhi.io ``` Add the following three files to your `python-docker-example` directory. The `Dockerfile` describes how to build the image, `compose.yaml` defines how Docker runs the container, and `.dockerignore` keeps unwanted files out of the build context. > [!TIP] > > [Gordon](/ai/gordon/), Docker's AI assistant, can generate Docker assets for > your project. Ask Gordon to create a Dockerfile, Compose file, and > `.dockerignore` tailored to your application. {{< files name="python-docker-example" >}} {{< file path="app.py" >}} ```python # A minimal FastAPI application. # The root endpoint (GET /) returns a JSON "Hello World" response. # See https://fastapi.tiangolo.com/ for the framework reference. from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"} ``` {{< /file >}} {{< file path="requirements.txt" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 uvicorn==0.34.3 ``` {{< /file >}} {{< file path="Dockerfile" status="new" >}} ```dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # This Dockerfile uses Docker Hardened Images (DHI) for enhanced security. # For more information, see https://docs.docker.com/dhi/ # Use the dev image to build and install dependencies. FROM dhi.io/python:3.12-dev AS builder WORKDIR /app RUN python3 -m venv /venv ENV PATH="/venv/bin:$PATH" # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them into # this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=requirements.txt,target=requirements.txt \ pip install -r requirements.txt # Use the minimal runtime image. It runs as nonroot by default. FROM dhi.io/python:3.12 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:$PATH" # Copy the source code into the container. COPY . . # Expose the port that the application listens on. EXPOSE 8000 # Run the application. CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] ``` {{< /file >}} {{< file path="compose.yaml" status="new" >}} ```yaml # Comments are provided throughout this file to help you get started. # If you need more help, visit the Docker Compose reference guide at # https://docs.docker.com/go/compose-spec-reference/ # Here the instructions define your application as a service called "server". # This service is built from the Dockerfile in the current directory. # You can add other services your application may depend on here, such as a # database or a cache. For examples, see the Awesome Compose repository: # https://github.com/docker/awesome-compose services: server: build: context: . ports: - 8000:8000 ``` {{< /file >}} {{< file path=".dockerignore" status="new" >}} ```text # Include any files or directories that you don't want to be copied to your # container here (e.g., local build artifacts, temporary files, etc.). # # For more help, visit the .dockerignore file reference guide at # https://docs.docker.com/go/build-context-dockerignore/ **/.DS_Store **/__pycache__ **/.venv **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/bin **/charts **/docker-compose* **/compose.y*ml **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ``` {{< /file >}} {{< file path=".gitignore" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} To learn more about each file, see the following: - [Dockerfile](/reference/dockerfile.md) - [.dockerignore](/reference/dockerfile.md#dockerignore-file) - [compose.yaml](/reference/compose-file/_index.md) ### Run the application Inside the `python-docker-example` directory, run the following command in a terminal. ```console $ docker compose up --build ``` Open a browser and view the application at [http://localhost:8000](http://localhost:8000). You should see a simple FastAPI application. In the terminal, press `ctrl`+`c` to stop the application. #### Run the application in the background You can run the application detached from the terminal by adding the `-d` option. Inside the `python-docker-example` directory, run the following command in a terminal. ```console $ docker compose up --build -d ``` Open a browser and view the application at [http://localhost:8000](http://localhost:8000). To see the OpenAPI docs you can go to [http://localhost:8000/docs](http://localhost:8000/docs). You should see a simple FastAPI application. In the terminal, run the following command to stop the application. ```console $ docker compose down ``` For more information about Compose commands, see the [Compose CLI reference](/reference/cli/docker/compose/). ## Use containers for Python development ### Prerequisites Complete [Containerize a Python application](#containerize-a-python-application). ### Overview Once your application runs in a container, the next step is making the container loop part of your everyday development workflow. Code changes should show up quickly, and services your app depends on, like databases, should run right alongside it. In this section, you'll extend the project from the previous topic by adding a PostgreSQL database service to your `compose.yaml`, persisting the database data in a named volume, and enabling Compose Watch so that changes you save in your editor are picked up by the running container without a manual rebuild. ### Update the application You'll update your application to connect to a PostgreSQL database. Continue working in your `python-docker-example` directory. Replace `app.py` and `requirements.txt`, and add a new `config.py` file with the following contents. > [!NOTE] > > The application won't run yet after this step. It tries to connect to a > PostgreSQL database that doesn't exist. The next two sections add the > database service and the Docker configuration needed to run everything > together. {{< files name="python-docker-example" >}} {{< file path="app.py" status="modified" >}} ```python # FastAPI application backed by a PostgreSQL database via SQLModel. # The FastAPI lifespan handler creates database tables at startup. # Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list). # See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/ from collections.abc import AsyncGenerator, Sequence from contextlib import asynccontextmanager from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select from config import settings class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) def create_db_and_tables() -> None: SQLModel.metadata.create_all(engine) @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: create_db_and_tables() yield app = FastAPI(lifespan=lifespan) @app.get("/") def hello() -> str: return "Hello, Docker!" @app.post("/heroes/") def create_hero(hero: Hero) -> Hero: with Session(engine) as session: session.add(hero) session.commit() session.refresh(hero) return hero @app.get("/heroes/") def read_heroes() -> Sequence[Hero]: with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes ``` {{< /file >}} {{< file path="config.py" status="new" >}} ```python # Pydantic settings that read PostgreSQL connection details from the # environment. Supports a password file (Docker secrets) via # POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD. # See https://docs.pydantic.dev/latest/concepts/pydantic_settings/ import os from typing import Any from pydantic import ( PostgresDsn, computed_field, field_validator, model_validator, ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings class Settings(BaseSettings): POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str | None = None POSTGRES_PASSWORD_FILE: str | None = None POSTGRES_DB: str @model_validator(mode="before") @classmethod def check_postgres_password(cls, data: Any) -> Any: """Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set.""" if isinstance(data, dict): password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore if password_file is None and password is None: raise ValueError( "At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set." ) return data # type: ignore @field_validator("POSTGRES_PASSWORD_FILE", mode="before") @classmethod def read_password_from_file(cls, v: str | None) -> str | None: if v is not None: file_path = v if os.path.exists(file_path): with open(file_path) as file: return file.read().strip() raise ValueError(f"Password file {file_path} does not exist.") return v @computed_field @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: url = MultiHostUrl.build( scheme="postgresql+psycopg", username=self.POSTGRES_USER, password=self.POSTGRES_PASSWORD if self.POSTGRES_PASSWORD else self.POSTGRES_PASSWORD_FILE, host=self.POSTGRES_SERVER, port=self.POSTGRES_PORT, path=self.POSTGRES_DB, ) return PostgresDsn(url) settings = Settings() # type: ignore ``` {{< /file >}} {{< file path="requirements.txt" status="modified" hl_lines="5-7" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 sqlmodel==0.0.24 psycopg[binary]==3.2.9 pydantic-settings==2.9.1 uvicorn==0.34.3 ``` {{< /file >}} {{< file path="Dockerfile" >}} ```dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # This Dockerfile uses Docker Hardened Images (DHI) for enhanced security. # For more information, see https://docs.docker.com/dhi/ # Use the dev image to build and install dependencies. FROM dhi.io/python:3.12-dev AS builder WORKDIR /app RUN python3 -m venv /venv ENV PATH="/venv/bin:$PATH" # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them into # this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=requirements.txt,target=requirements.txt \ pip install -r requirements.txt # Use the minimal runtime image. It runs as nonroot by default. FROM dhi.io/python:3.12 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:$PATH" # Copy the source code into the container. COPY . . # Expose the port that the application listens on. EXPOSE 8000 # Run the application. CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] ``` {{< /file >}} {{< file path="compose.yaml" >}} ```yaml # Comments are provided throughout this file to help you get started. # If you need more help, visit the Docker Compose reference guide at # https://docs.docker.com/go/compose-spec-reference/ # Here the instructions define your application as a service called "server". # This service is built from the Dockerfile in the current directory. # You can add other services your application may depend on here, such as a # database or a cache. For examples, see the Awesome Compose repository: # https://github.com/docker/awesome-compose services: server: build: context: . ports: - 8000:8000 ``` {{< /file >}} {{< file path=".dockerignore" >}} ```text # Include any files or directories that you don't want to be copied to your # container here (e.g., local build artifacts, temporary files, etc.). # # For more help, visit the .dockerignore file reference guide at # https://docs.docker.com/go/build-context-dockerignore/ **/.DS_Store **/__pycache__ **/.venv **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/bin **/charts **/docker-compose* **/compose.y*ml **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ``` {{< /file >}} {{< file path=".gitignore" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} ### Update Docker assets Replace `Dockerfile` and `compose.yaml` with the following. {{< files name="python-docker-example" >}} {{< file path="app.py" >}} ```python # FastAPI application backed by a PostgreSQL database via SQLModel. # The FastAPI lifespan handler creates database tables at startup. # Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list). # See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/ from collections.abc import AsyncGenerator, Sequence from contextlib import asynccontextmanager from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select from config import settings class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) def create_db_and_tables() -> None: SQLModel.metadata.create_all(engine) @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: create_db_and_tables() yield app = FastAPI(lifespan=lifespan) @app.get("/") def hello() -> str: return "Hello, Docker!" @app.post("/heroes/") def create_hero(hero: Hero) -> Hero: with Session(engine) as session: session.add(hero) session.commit() session.refresh(hero) return hero @app.get("/heroes/") def read_heroes() -> Sequence[Hero]: with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes ``` {{< /file >}} {{< file path="config.py" >}} ```python # Pydantic settings that read PostgreSQL connection details from the # environment. Supports a password file (Docker secrets) via # POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD. # See https://docs.pydantic.dev/latest/concepts/pydantic_settings/ import os from typing import Any from pydantic import ( PostgresDsn, computed_field, field_validator, model_validator, ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings class Settings(BaseSettings): POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str | None = None POSTGRES_PASSWORD_FILE: str | None = None POSTGRES_DB: str @model_validator(mode="before") @classmethod def check_postgres_password(cls, data: Any) -> Any: """Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set.""" if isinstance(data, dict): password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore if password_file is None and password is None: raise ValueError( "At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set." ) return data # type: ignore @field_validator("POSTGRES_PASSWORD_FILE", mode="before") @classmethod def read_password_from_file(cls, v: str | None) -> str | None: if v is not None: file_path = v if os.path.exists(file_path): with open(file_path) as file: return file.read().strip() raise ValueError(f"Password file {file_path} does not exist.") return v @computed_field @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: url = MultiHostUrl.build( scheme="postgresql+psycopg", username=self.POSTGRES_USER, password=self.POSTGRES_PASSWORD if self.POSTGRES_PASSWORD else self.POSTGRES_PASSWORD_FILE, host=self.POSTGRES_SERVER, port=self.POSTGRES_PORT, path=self.POSTGRES_DB, ) return PostgresDsn(url) settings = Settings() # type: ignore ``` {{< /file >}} {{< file path="requirements.txt" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 sqlmodel==0.0.24 psycopg[binary]==3.2.9 pydantic-settings==2.9.1 uvicorn==0.34.3 ``` {{< /file >}} {{< file path="Dockerfile" status="modified" hl_lines="11,27-34,37,45" >}} ```dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # This Dockerfile uses Docker Hardened Images (DHI) for enhanced security. # For more information, see https://docs.docker.com/dhi/ # Use the dev image to build and install dependencies. # The builder stage is also used directly in development (see compose.yaml). FROM dhi.io/python:3.12-dev AS builder WORKDIR /app RUN python3 -m venv /venv ENV PATH="/venv/bin:$PATH" # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them # into this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=requirements.txt,target=requirements.txt \ pip install -r requirements.txt # Copy the source code into the container. COPY . . # Expose the port that the application listens on. EXPOSE 8000 # Run the application. CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] # Use the minimal runtime image for production. It runs as nonroot by default. FROM dhi.io/python:3.12 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:$PATH" COPY --from=builder /app . EXPOSE 8000 CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] ``` {{< /file >}} {{< file path="compose.yaml" status="modified" hl_lines="8" >}} ```yaml services: # Application service. The `target: builder` line builds the development # image (includes a shell and tools); the production stage of the # Dockerfile is unused in development. server: build: context: . target: builder ports: - 8000:8000 ``` {{< /file >}} {{< file path=".dockerignore" >}} ```text # Include any files or directories that you don't want to be copied to your # container here (e.g., local build artifacts, temporary files, etc.). # # For more help, visit the .dockerignore file reference guide at # https://docs.docker.com/go/build-context-dockerignore/ **/.DS_Store **/__pycache__ **/.venv **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/bin **/charts **/docker-compose* **/compose.y*ml **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ``` {{< /file >}} {{< file path=".gitignore" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} #### About these changes The `Dockerfile` builder stage now includes `COPY . .` and a `CMD` instruction, which makes it directly runnable. This lets Compose target the builder stage during development without rebuilding the production stage. The production stage at the bottom is unchanged and still produces a minimal, nonroot runtime image for shipping. In `compose.yaml`, the new `target: builder` line tells Compose to build and run the builder stage of the Dockerfile during development. Unlike the minimal production image, the development image includes a shell and additional tools that make debugging easier. If you need a shell in a running production container, use [Docker Debug](/reference/cli/docker/debug/) instead. ### Add a local database and persist data You can use containers to set up local services, like a database. In this section, you'll update the `compose.yaml` file to define a database service and a volume to persist data, and add a `db/password.txt` file that holds the database password. {{< files name="python-docker-example" >}} {{< file path="app.py" >}} ```python # FastAPI application backed by a PostgreSQL database via SQLModel. # The FastAPI lifespan handler creates database tables at startup. # Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list). # See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/ from collections.abc import AsyncGenerator, Sequence from contextlib import asynccontextmanager from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select from config import settings class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) def create_db_and_tables() -> None: SQLModel.metadata.create_all(engine) @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: create_db_and_tables() yield app = FastAPI(lifespan=lifespan) @app.get("/") def hello() -> str: return "Hello, Docker!" @app.post("/heroes/") def create_hero(hero: Hero) -> Hero: with Session(engine) as session: session.add(hero) session.commit() session.refresh(hero) return hero @app.get("/heroes/") def read_heroes() -> Sequence[Hero]: with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes ``` {{< /file >}} {{< file path="config.py" >}} ```python # Pydantic settings that read PostgreSQL connection details from the # environment. Supports a password file (Docker secrets) via # POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD. # See https://docs.pydantic.dev/latest/concepts/pydantic_settings/ import os from typing import Any from pydantic import ( PostgresDsn, computed_field, field_validator, model_validator, ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings class Settings(BaseSettings): POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str | None = None POSTGRES_PASSWORD_FILE: str | None = None POSTGRES_DB: str @model_validator(mode="before") @classmethod def check_postgres_password(cls, data: Any) -> Any: """Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set.""" if isinstance(data, dict): password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore if password_file is None and password is None: raise ValueError( "At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set." ) return data # type: ignore @field_validator("POSTGRES_PASSWORD_FILE", mode="before") @classmethod def read_password_from_file(cls, v: str | None) -> str | None: if v is not None: file_path = v if os.path.exists(file_path): with open(file_path) as file: return file.read().strip() raise ValueError(f"Password file {file_path} does not exist.") return v @computed_field @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: url = MultiHostUrl.build( scheme="postgresql+psycopg", username=self.POSTGRES_USER, password=self.POSTGRES_PASSWORD if self.POSTGRES_PASSWORD else self.POSTGRES_PASSWORD_FILE, host=self.POSTGRES_SERVER, port=self.POSTGRES_PORT, path=self.POSTGRES_DB, ) return PostgresDsn(url) settings = Settings() # type: ignore ``` {{< /file >}} {{< file path="requirements.txt" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 sqlmodel==0.0.24 psycopg[binary]==3.2.9 pydantic-settings==2.9.1 uvicorn==0.34.3 ``` {{< /file >}} {{< file path="Dockerfile" >}} ```dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # This Dockerfile uses Docker Hardened Images (DHI) for enhanced security. # For more information, see https://docs.docker.com/dhi/ # Use the dev image to build and install dependencies. # The builder stage is also used directly in development (see compose.yaml). FROM dhi.io/python:3.12-dev AS builder WORKDIR /app RUN python3 -m venv /venv ENV PATH="/venv/bin:$PATH" # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them # into this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=requirements.txt,target=requirements.txt \ pip install -r requirements.txt # Copy the source code into the container. COPY . . # Expose the port that the application listens on. EXPOSE 8000 # Run the application. CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] # Use the minimal runtime image for production. It runs as nonroot by default. FROM dhi.io/python:3.12 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:$PATH" COPY --from=builder /app . EXPOSE 8000 CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] ``` {{< /file >}} {{< file path="compose.yaml" status="modified" hl_lines="11-46" >}} ```yaml services: # Application service. The `target: builder` line builds the development # image (includes a shell and tools); the production stage of the # Dockerfile is unused in development. server: build: context: . target: builder ports: - 8000:8000 environment: - POSTGRES_SERVER=db - POSTGRES_USER=postgres - POSTGRES_DB=example - POSTGRES_PASSWORD_FILE=/run/secrets/db-password depends_on: db: condition: service_healthy secrets: - db-password # Database service. Reads the password from a Docker secret mounted at # /run/secrets/db-password. Compose waits for the healthcheck to pass # before starting the server, via the server's depends_on. db: image: dhi.io/postgres:18 restart: always user: postgres secrets: - db-password volumes: - db-data:/var/lib/postgresql environment: - POSTGRES_DB=example - POSTGRES_PASSWORD_FILE=/run/secrets/db-password expose: - 5432 healthcheck: test: ["CMD", "pg_isready"] interval: 10s timeout: 5s retries: 5 volumes: db-data: secrets: db-password: file: db/password.txt ``` {{< /file >}} {{< file path="db/password.txt" status="new" >}} ```text mysecretpassword ``` {{< /file >}} {{< file path=".dockerignore" >}} ```text # Include any files or directories that you don't want to be copied to your # container here (e.g., local build artifacts, temporary files, etc.). # # For more help, visit the .dockerignore file reference guide at # https://docs.docker.com/go/build-context-dockerignore/ **/.DS_Store **/__pycache__ **/.venv **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/bin **/charts **/docker-compose* **/compose.y*ml **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ``` {{< /file >}} {{< file path=".gitignore" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} > [!NOTE] > > To learn more about the instructions in the Compose file, see [Compose file > reference](/reference/compose-file/). Now, run the following `docker compose up` command to start your application. ```console $ docker compose up --build ``` Now test your API endpoint. Open a new terminal then make a request to the server using the curl commands: Create an object with a POST request: ```console $ curl -X 'POST' \ 'http://localhost:8000/heroes/' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "name": "my hero", "secret_name": "austing", "age": 12 }' ``` You should receive the following response: ```json { "age": 12, "id": 1, "name": "my hero", "secret_name": "austing" } ``` Now make a GET request: ```console $ curl -X 'GET' \ 'http://localhost:8000/heroes/' \ -H 'accept: application/json' ``` You should receive the same response as above because it's the only object in the database. ```json { "age": 12, "id": 1, "name": "my hero", "secret_name": "austing" } ``` Press `ctrl+c` in the terminal to stop your application. ### Automatically update services Use Compose Watch to automatically update your running Compose services as you edit and save your code. For more details about Compose Watch, see [Use Compose Watch](/manuals/compose/how-tos/file-watch.md). Open your `compose.yaml` file in an IDE or text editor and add the highlighted Compose Watch instructions. {{< files name="python-docker-example" >}} {{< file path="app.py" >}} ```python # FastAPI application backed by a PostgreSQL database via SQLModel. # The FastAPI lifespan handler creates database tables at startup. # Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list). # See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/ from collections.abc import AsyncGenerator, Sequence from contextlib import asynccontextmanager from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select from config import settings class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) def create_db_and_tables() -> None: SQLModel.metadata.create_all(engine) @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: create_db_and_tables() yield app = FastAPI(lifespan=lifespan) @app.get("/") def hello() -> str: return "Hello, Docker!" @app.post("/heroes/") def create_hero(hero: Hero) -> Hero: with Session(engine) as session: session.add(hero) session.commit() session.refresh(hero) return hero @app.get("/heroes/") def read_heroes() -> Sequence[Hero]: with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes ``` {{< /file >}} {{< file path="config.py" >}} ```python # Pydantic settings that read PostgreSQL connection details from the # environment. Supports a password file (Docker secrets) via # POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD. # See https://docs.pydantic.dev/latest/concepts/pydantic_settings/ import os from typing import Any from pydantic import ( PostgresDsn, computed_field, field_validator, model_validator, ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings class Settings(BaseSettings): POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str | None = None POSTGRES_PASSWORD_FILE: str | None = None POSTGRES_DB: str @model_validator(mode="before") @classmethod def check_postgres_password(cls, data: Any) -> Any: """Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set.""" if isinstance(data, dict): password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore if password_file is None and password is None: raise ValueError( "At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set." ) return data # type: ignore @field_validator("POSTGRES_PASSWORD_FILE", mode="before") @classmethod def read_password_from_file(cls, v: str | None) -> str | None: if v is not None: file_path = v if os.path.exists(file_path): with open(file_path) as file: return file.read().strip() raise ValueError(f"Password file {file_path} does not exist.") return v @computed_field @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: url = MultiHostUrl.build( scheme="postgresql+psycopg", username=self.POSTGRES_USER, password=self.POSTGRES_PASSWORD if self.POSTGRES_PASSWORD else self.POSTGRES_PASSWORD_FILE, host=self.POSTGRES_SERVER, port=self.POSTGRES_PORT, path=self.POSTGRES_DB, ) return PostgresDsn(url) settings = Settings() # type: ignore ``` {{< /file >}} {{< file path="requirements.txt" >}} ```text # Python package dependencies for the application, pinned for reproducible builds. # See https://pip.pypa.io/en/stable/reference/requirements-file-format/ fastapi==0.115.12 sqlmodel==0.0.24 psycopg[binary]==3.2.9 pydantic-settings==2.9.1 uvicorn==0.34.3 ``` {{< /file >}} {{< file path="Dockerfile" >}} ```dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # This Dockerfile uses Docker Hardened Images (DHI) for enhanced security. # For more information, see https://docs.docker.com/dhi/ # Use the dev image to build and install dependencies. # The builder stage is also used directly in development (see compose.yaml). FROM dhi.io/python:3.12-dev AS builder WORKDIR /app RUN python3 -m venv /venv ENV PATH="/venv/bin:$PATH" # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them # into this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=requirements.txt,target=requirements.txt \ pip install -r requirements.txt # Copy the source code into the container. COPY . . # Expose the port that the application listens on. EXPOSE 8000 # Run the application. CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] # Use the minimal runtime image for production. It runs as nonroot by default. FROM dhi.io/python:3.12 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:$PATH" COPY --from=builder /app . EXPOSE 8000 CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"] ``` {{< /file >}} {{< file path="compose.yaml" status="modified" hl_lines="21-24" >}} ```yaml services: # Application service. The `target: builder` line builds the development # image (includes a shell and tools); the production stage of the # Dockerfile is unused in development. server: build: context: . target: builder ports: - 8000:8000 environment: - POSTGRES_SERVER=db - POSTGRES_USER=postgres - POSTGRES_DB=example - POSTGRES_PASSWORD_FILE=/run/secrets/db-password depends_on: db: condition: service_healthy secrets: - db-password develop: watch: - action: rebuild path: . db: image: dhi.io/postgres:18 restart: always user: postgres secrets: - db-password volumes: - db-data:/var/lib/postgresql environment: - POSTGRES_DB=example - POSTGRES_PASSWORD_FILE=/run/secrets/db-password expose: - 5432 healthcheck: test: ["CMD", "pg_isready"] interval: 10s timeout: 5s retries: 5 volumes: db-data: secrets: db-password: file: db/password.txt ``` {{< /file >}} {{< file path="db/password.txt" >}} ```text mysecretpassword ``` {{< /file >}} {{< file path=".dockerignore" >}} ```text # Include any files or directories that you don't want to be copied to your # container here (e.g., local build artifacts, temporary files, etc.). # # For more help, visit the .dockerignore file reference guide at # https://docs.docker.com/go/build-context-dockerignore/ **/.DS_Store **/__pycache__ **/.venv **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/bin **/charts **/docker-compose* **/compose.y*ml **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ``` {{< /file >}} {{< file path=".gitignore" >}} ```text # Files and directories that Git should ignore. This is the standard Python # template covering bytecode, build artifacts, virtual environments, and IDE # settings. See https://git-scm.com/docs/gitignore for syntax reference. # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Secrets db/password.txt ``` {{< /file >}} {{< /files >}} Run the following command to run your application with Compose Watch. ```console $ docker compose watch ``` In a terminal, curl the application to get a response. ```console $ curl http://localhost:8000 Hello, Docker! ``` Any changes to the application's source files on your local machine will now be immediately reflected in the running container. Open `python-docker-example/app.py` in an IDE or text editor and update the `Hello, Docker!` string by adding a few more exclamation marks. ```diff - return 'Hello, Docker!' + return 'Hello, Docker!!!' ``` Save the changes to `app.py` and then wait a few seconds for the application to rebuild. Curl the application again and verify that the updated text appears. ```console $ curl http://localhost:8000 Hello, Docker!!! ``` Press `ctrl+c` in the terminal to stop your application. ## Linting, formatting, and type checking for Python ### Prerequisites Complete [Develop your app](#use-containers-for-python-development). This topic requires a local Python installation because the tools and Git hooks introduced here run on your host. If you don't want to install Python locally, skip this topic. You can also run the same checks in CI. ### Overview Linting, formatting, and type checking are automated ways to catch bugs, enforce style, and spot type errors before code runs. Running them on every commit, in CI, and in your editor catches problems early when they're cheap to fix. In this section, you'll configure three tools for your Python application. Ruff handles linting and formatting in a single fast pass. Pyright statically checks your code for type errors. Pre-commit hooks run both of these automatically before each Git commit so problems are caught locally before they're committed. ### Linting and formatting with Ruff Ruff is an extremely fast Python linter and formatter written in Rust. It replaces multiple tools like flake8, isort, and black with a single unified tool. Create a `pyproject.toml` file in your `python-docker-example` directory: {{< files name="python-docker-example" >}} {{< file path="pyproject.toml" status="new" >}} ```toml # Configuration for code-quality tools. # - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/) # - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/) [tool.ruff] target-version = "py312" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade "ARG001", # unused arguments in functions ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "W191", # indentation contains tabs "B904", # Allow raising exceptions without from e, for HTTPException ] ``` {{< /file >}} {{< /files >}} Install Ruff: ```console $ pip install ruff ``` If you're using a virtual environment, make sure it is activated so the `ruff` command is available. Run these commands to check and format your code: ```console # Check for errors $ ruff check . # Automatically fix fixable errors $ ruff check --fix . # Format code $ ruff format . ``` ### Type checking with Pyright Pyright is a fast static type checker for Python that works well with modern Python features. Update `pyproject.toml` to add the Pyright configuration at the bottom. {{< files name="python-docker-example" >}} {{< file path="pyproject.toml" status="modified" hl_lines="25-29" >}} ```toml # Configuration for code-quality tools. # - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/) # - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/) [tool.ruff] target-version = "py312" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade "ARG001", # unused arguments in functions ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "W191", # indentation contains tabs "B904", # Allow raising exceptions without from e, for HTTPException ] [tool.pyright] typeCheckingMode = "strict" pythonVersion = "3.12" exclude = [".venv"] ``` {{< /file >}} {{< /files >}} Install Pyright and run it: ```console $ pip install pyright $ pyright ``` ### Setting up pre-commit hooks Pre-commit hooks run checks automatically before each commit on your local machine. Create a `.pre-commit-config.yaml` file in your `python-docker-example` directory to set up Ruff hooks: {{< files name="python-docker-example" >}} {{< file path=".pre-commit-config.yaml" status="new" >}} ```yaml # Pre-commit hook configuration. Runs Ruff (lint + format) on every # `git commit`. See https://pre-commit.com/ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.15 hooks: - id: ruff args: [--fix] - id: ruff-format ``` {{< /file >}} {{< /files >}} To install and use: ```console $ pip install pre-commit $ pre-commit install $ git commit -m "Test commit" # Automatically runs checks ``` ### Summary In this section, you learned how to: - Configure and use Ruff for linting and formatting - Set up Pyright for static type checking - Automate checks with pre-commit hooks These tools help maintain code quality and catch errors early in development. Related information: - [Ruff documentation](https://docs.astral.sh/ruff/) - [Pyright documentation](https://microsoft.github.io/pyright/) - [pre-commit framework](https://pre-commit.com/) ### Next steps - Customize linting rules to match your team's style preferences - Explore advanced type checking features