ABEJA Tech Blog

中の人の興味のある情報を発信していきます

環境構築を爆速で!Python開発テンプレートのご紹介

こんにちは!システム開発部でエンジニアをやっている春名です。
この記事はABEJAアドベントカレンダー2025の19日目の記事です。

Pythonの開発環境については、過去に以下のような記事を書きました。

tech-blog.abeja.asia

ユーモアあふれる記事が並ぶ中ではやや地味なテーマですが、この記事の初公開から早くも2年が経ちました。 その間に環境もいくつか変化しているため、改めて現在システム開発部で利用している開発環境をご紹介します!

前回からの大きな変化点で言うと以下になります。

  • パッケージマネージャがpoetryに変わりuvが主流になった
  • ruffにformatterが取り込まれblackを個別に使う必要がなくなった

それに加えてシステム開発部では、これらの変更を取り込んだPythonのバックエンド開発環境を cookiecutterで利用できるように整備しています。

これによりプロジェクト開始時のバックエンドの構築が爆速で終わるようになっています🚀

目次

今回作成する環境

前回と同様にFastAPIを利用してAPIを提供するバックエンド環境を作成する場合を例にしてご紹介します。
以下のツールを導入し、その環境を cookiecutterのテンプレートとして使えるように作成していきます。

リンターとフォーマッターがRuffに統合されたので、だいぶスッキリしましたね。
あとは今の構成だとmypyによる型チェックに時間がかかっているので、移行先としてtyに注目しています。
まだalpha版ですが、uvやruffも開発しているAstralが開発する型チェッカーなので、相性良く使えそうな気がしています。

使用しているPC環境は以下の通りです。

  • MacBook Pro 2022 (M2)
  • Python 3.11

cookiecutter のテンプレート作成

まずはcookiecutterのテンプレート作成から行います。

cookiecutterのインストール

公式に記載されているpipxやpipからのインストールや、Macの場合はbrewを使ってもインストールできます。

pipx install cookiecutter

or 

brew install cookiecutter

テンプレートディレクトリの作成

ここでは {{cookiecutter.project_slug}}というディレクトリを作成し、テンプレートタグでプロジェクトのスラッグ名が指定できるようにしておきます。

mkdir -p fastapi-backend-template/{{cookiecutter.project_slug}}

cookiecutter.jsonの作成

作成したディレクトリのルートに cookiecutter.json というファイルを作成します。

cd fastapi-backend-template
touch cookiecutter.json

cookiecutter.jsonにはテンプレートとして使用するタグの内容を記述します。

{
  "project_name": "FastAPI Backend",
  "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}"
}

project_slugにはプロジェクト名のスペースをハイフンに変換したものがデフォルトで入力されるようにしています。

uvによるプロジェクトの作成

uvのインストール

公式のインストール手順に従いインストールします。

$ curl -LsSf https://astral.sh/uv/install.sh | sh
$ uv --version
uv 0.9.15 (5eafae332 2025-12-02)

プロジェクトの作成

プロジェクトスラッグ名のディレクトリ内にuvでプロジェクトを作成します。

$ cd {{cookiecutter.project_slug}}
$ uv init backend
$ cd backend
$ tree .
backend
├── README.md
├── main.py
└── pyproject.toml
1 directory, 3 files

依存関係の追加

この辺りはパッケージマネージャーがuvに変わっただけで、やっていることは前回と同じです。

FastAPI

uv install "fastapi[all]"

Ruff / mypy / pytest

uv add ruff mypy pytest --dev

hadolint

brew install hadolint

Hadolintの設定も特に変わっていません。

.hadolint.yaml

ignored:
  - DL3008 # Pin versions in apt get install

pyptoject.tomlの更新

pyptoject.tomlへRuffやmypyなどの設定を行なっていきます。

name: "{{cookiecutter.project_slug}}"という記述にすることで、作成したプロジェクトのスラッグ名が反映されるようにしています。
あとはRuffの設定記述方法が少し変わっていたり、black個別の設定がなくなっていたりしますが基本的には前回と同じ内容です。

[project]
name = "{{cookiecutter.project_slug}}"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi[all]>=0.123.9",
]

[project.optional-dependencies]
dev = [
    "mypy>=1.19.0",
    "ruff>=0.14.8",
]
test = [
    "pytest>=9.0.1",
]

[tool.ruff]
target-version = "py311"
line-length = 100

[tool.ruff.lint]
select = [
    "E", # pycodestyle errors
    "W", # pycodestyle warnings
    "F", # pyflakes
    "B", # flake8-bugbear
    "I", # isort
]
ignore = [
    "E501", # line too long, handled by black
    "B008", # do not perform function calls in argument defaults
]
unfixable = [
    "F401", # module imported but unused
    "F841", # local variable is assigned to but never used
]

[tool.mypy]
python_version = "3.11"
strict = true

[tool.uv]
package = false

FastAPIの処理を実装

あとはテンプレートにしたいFastAPIの処理を実装をしていきます。

システム開発部のテンプレートでは、docker-composeによるpostgresの起動や、SQLAlchemyを使用したユーザーに対するCRUD、almebicによるDBマイグレーションなど、殆どのプロジェクトで使用するであろう構成をテンプレートとして実装してあります。

ここでは、シンプルなヘルスチェックのエンドポイントのみを持ったコードを例として記載します。

rm main.py
mkdir -p src/routers src/schemas
touch src/__init__.py
touch src/main.py
touch src/schemas/health_check.py
touch src/routers/health_check.py

src/main.py

from fastapi import FastAPI

from src.routers import health_check

app = FastAPI()


app.include_router(health_check.router)

routers/health_check.py

from fastapi import APIRouter

from src.schemas.health_check import HealthCheck

router = APIRouter()


@router.get("/health-check", response_model=HealthCheck)
def health_check() -> HealthCheck:
    return HealthCheck(status="ok")

schemas/health_check.py

from pydantic import BaseModel


class HealthCheck(BaseModel):
    status: str

実装が完了したら以下で起動して動作確認ができます。

uv run uvicorn src.main:app --host 0.0.0.0 --reload
$ curl http://127.0.0.1:8000/health-check                                          
{"status":"ok"}

単体テストの実装

pytestでテストするコードの実装です。 テストしている内容は前回と同じですが、テストケースを追加しやすいようクラスにまとめたり@pytest.mark.parametrizeなどを使うことで拡張しやすい形でテンプレートには実装してあります。

mkdir -p tests/unit/routers
touch tests/__init__.py
touch tests/unit/__init__.py
touch tests/unit/routers/__init__.py
touch tests/unit/routers/conftest.py
touch tests/unit/routers/test_health_check.py

tests/unit/routers/conftest.py

from typing import Generator

import pytest
from fastapi.testclient import TestClient

from src.main import app


@pytest.fixture
def client() -> Generator[TestClient, None, None]:
    # NOTE: Add overrides to app here if needed.

    with TestClient(app, raise_server_exceptions=False) as client:
        yield client

tests/unit/test_health_check.py

import pytest
from fastapi.testclient import TestClient


class TestGetHealthCheck:
    @pytest.mark.parametrize(
        "expected_status_code, expected_response",
        [
            (200, {"status": "ok"}),
        ],
    )
    def test_valid(
        self,
        client: TestClient,
        expected_status_code: int,
        expected_response: dict[str, str],
    ) -> None:
        response = client.get("/health-check")

        assert response.status_code == expected_status_code
        assert response.json() == expected_response

単体テスト実行

pytestコマンドで実行し、PASSEDになることを確認します。

$ uv run pytest tests/
==================================== test session starts ====================================
platform darwin -- Python 3.11.8, pytest-9.0.1, pluggy-1.6.0 -- /Users/user/fastapi-backend-template/{{cookiecutter.project_slug}}/backend/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/user/fastapi-backend-template/{{cookiecutter.project_slug}}/backend
configfile: pyproject.toml
plugins: anyio-4.12.0
collected 1 item

tests/unit/routers/test_health_check.py::TestGetHealthCheck::test_valid[200-expected_response0] PASSED [100%]

===================================== 1 passed in 0.27s =====================================

これでテンプレートなるFastAPI環境の構築は完了です。
確認が終わったらテンプレートには不要なファイルは削除しておきましょう。

rm -fr .venv
find . -name '__pycache__' -type d -exec rm -r {} +

cookiecutterでテンプレートを使う

作成したテンプレートをcookiecutterで使ってみます。

使用する際はcookiecutterで作成したテンプレートのディレクトリを指定するだけです。

$ cookiecutter fastapi-backend-template
  [1/2] project_name (FastAPI Backend): DX Project
  [2/2] project_slug (dx-project):

実行すると設定したタグに対するプロンプトが表示されるので、それらを入力するとプロジェクトディレクトリが作成されます。

$ tree dx-project -d
dx-project
└── backend
    ├── src
    │   ├── routers
    │   └── schemas
    └── tests
        └── unit
            └── routers

できました!

あとはこのテンプレートをリポジトリに登録しておけば、以下のように利用可能です。

cookiecutter git@github.com:your-org/fastapi-backend-template

or

cookiecutter https://github.com/your-org/fastapi-backend-template

cookiecutterのHooks機能

cookiecutterにはテンプレートからプロジェクトを作る特定のタイミングで任意の処理を実行するHooks機能があります。

テンプレートディレクトリのルートに hooksというディレクトリを作成し、以下のファイル名を持つスクリプトを配置することで実行されます。
今回作成したテンプレートに配置する場合は以下のような形です。(.shも配置可能ですが公式では.pyを推奨)

fastapi-backend-template/
├── {{cookiecutter.project_slug}}/
├── hooks
│   ├── pre_prompt.py
│   ├── pre_gen_project.py
│   └── post_gen_project.py
└── cookiecutter.json

それぞれ3つのファイル名が意味するタイミングで、任意の処理を実行させることができます。 詳細は公式ドキュメントをご参照ください。

システム開発部では、この機能を利用してフロントエンドとバックエンドをモノレポで管理するテンプレートも作成しています。

  1. プロジェクト全体を管理するテンプレート
  2. フロントエンドを管理するテンプレート
  3. バックエンドを管理するテンプレート

これらのテンプレートをcookiecutterで作成し、1のテンプレートを実行してプロジェクトを作った際、post_gen_project.pyで残りの2、3のテンプレートを使ったプロジェクトを配下に作る、といった形です。

一部の抜粋になりますが、post_gen_project.py内でcookiecutterを使うことで複数のテンプレートを同時に展開しています。

#!/usr/bin/env python
import os
import subprocess
from pathlib import Path
from cookiecutter.main import cookiecutter
import shutil

PROJECT_DIR = Path(os.getcwd())
BACKEND_DIR = PROJECT_DIR / "backend"

cookiecutter(
    "git@github.com:your-org/fastapi-backend-template.git",
    checkout="main",
    no_input=True,
    output_dir=str(BACKEND_DIR.parent),
    extra_context={
        "project_slug": "{{ cookiecutter.project_slug }}",
    },
)

# backend/をプロジェクトのルートに移動
for item in (PROJECT_DIR / "{{ cookiecutter.project_slug }}" / "backend").iterdir():
    shutil.move(item, BACKEND_DIR / item.name)

# 不要になったプロジェクトディレクトリを削除
shutil.rmtree(PROJECT_DIR / "{{ cookiecutter.project_slug }}", ignore_errors=True)

このようにhooksを活用することで、それぞれのテンプレートを独立したリポジトリでメンテしつつ、利用する際は1度の実行で環境が整うようになっています!

おわりに

テンプレート化して再利用できるようにすることで、プロジェクト開始時の環境構築のコストはかなり下がったように感じます。 このテンプレートでAGENTS.mdなども管理し、AI駆動開発に対応しつつ運用中です。

再利用できるものは積極的にメンバー全員が協力して再利用できるような形に整備し、そこで節約できた時間をよりミッションクリティカルな部分の開発に注力することに使っていけると良いなと思っています。

We are hiring!

今回ご紹介したような開発効率化のための環境整備なども含め、一緒にテクノロジーの社会実装に取り組んでいただけるエンジニアを募集中です!

careers.abejainc.com

ソフトウェアエンジニアポジションはこちらです。

新卒の方もお待ちしております!