MaruOpsInfrastructure and AI notes

About (Korean)Contact (Korean)

[Python Environments 04] Structure a Web Project with src, uv and Tests

Desk with keyboard, notepad, and headphones

Reference photo · Desk with keyboard, notepad, and headphones · Photo: Teresa Kluge lum3n / Source / CC0 · Resized for listings; illustrative photo, not the tested environment

An API server, frontend build, database and background worker may need different tools and release cycles. Organize a small Python web project so these execution boundaries can be separated when needed. Begin with one API, a src layout, an explicit environment and tests.

Python development environment · 01 selection and venv · 02 uv · 03 conda · 04 web Development·05 Data·AI·06 OCR·Service Separation

On this page

The scope of the Python environment in web production

componentExamples of environmental management
Python web serverManage selected frameworks and DB drivers such as Django·Flask·FastAPI with uv or venv
Frontend for browsersNode.js and package.json · Manage the lock files of the corresponding ecosystem separately
Services such as DB and RedisManaged as a separate service or container. Python DB driver installation does not replace DB server installation
heavy workerDecide whether to separate OCR, model inference, etc. into a different environment and process from the API

The choice depends on whether to configure the management screen, authentication, and ORM within one framework, or whether to separate the API and front end. The FastAPI below is a small example to explain the structure of the development environment, and is not a tutorial comparing entire web frameworks. For how to use the API, refer to the official FastAPI document .

1. Separate source and execution environment

web-demo/
├── pyproject.toml
├── uv.lock
├── .python-version
├── .env.example
├── .gitignore
├── src/
│   └── web_demo/
│       ├── __init__.py
│       └── main.py
└── tests/
    └── test_health.py

The src/ structure reduces the problem of importing modules that happen to be in the repository root and is good for testing based on installed packages. Instead, the process of installing the project into the environment is required. Here we declare build-system and have uv sync install the project in editable form. You can refer to PyPA’s src·flat layout comparison.

2. Write pyproject.toml

[project]
name = "web-demo"
version = "0.1.0"
requires-python = ">=3.12,<3.13"
dependencies = ["fastapi>=0.115,<1", "uvicorn>=0.30,<1"]

[dependency-groups]
dev = ["pytest>=8,<10", "httpx>=0.27,<1", "ruff>=0.11,<1"]

[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
testpaths = ["tests"]

FastAPI and Uvicorn required for execution are placed in project.dependencies, and tests and lint tools are placed in the dev group. Hatchling is the build backend for installing this example as a Python package. Even without complex in-house deployments, specifying this boundary can save you from temporarily appending the src path to PYTHONPATH.

3. Save the API and test files

Create src/web_demo/__init__.py as an empty file, and enter the following content in src/web_demo/main.py.

from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}

tests/test_health.py stores tests that call the API.

from fastapi.testclient import TestClient

from web_demo.main import app


def test_health():
    with TestClient(app) as client:
        response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}
uv python pin 3.12
uv lock
uv sync --locked
uv run --locked python -m pytest
uv run --locked ruff check src tests
uv run --locked python -c 'import sys, web_demo; print(sys.executable); print(web_demo.__file__)'

Make sure the import path points to src/web_demo in the current project. The project deployment name is web-demo and the import name from Python is web_demo. If you confuse hyphens and underscores, a situation may arise where the package is installed but cannot be imported.

4. Run the development server

uv run --locked uvicorn web_demo.main:app --reload --host 127.0.0.1 --port 8000

If you run curl http://127.0.0.1:8000/health in another terminal on the same server, you can see the {"status":"ok"} response. If you run it on a remote VM, connect by sending it to ssh -N -L 18000:127.0.0.1:8000 labadmin@서버주소 from your PC. Here we do not turn off the server firewall or directly expose the development server to the public network.

--reload is a development setting that monitors source changes. Remove it from the operating environment and set up process management, HTTPS, reverse proxy, log, and DB connection settings. It’s important to distinguish between running a single line of development servers and completing a production deployment.

5. Set Python in the IDE to the same environment.

  • Select the Python interpreter in an editor, such as VS Code, as .venv/bin/python for your project.
  • Make sure the debugger and test execution settings use the same interpreter.
  • In the terminal, compare as uv run python -c "import sys; print(sys.executable)".
  • The configuration file saves only .env.example as a format example, and excludes the actual .env and credentials from Git.

6. Create a new operating environment without development tools

# 운영용 새 체크아웃의 프로젝트 루트
uv sync --locked --no-default-groups
.venv/bin/python -c 'from web_demo.main import app; print(app.title)'

Subsequent runs of the service are less dependent on the shell’s activation state by using an explicit executable path. The following systemd example is a template to be modified to suit the server where the webapp account, project directory, and environment files have been prepared in advance. This is not an example of running systemd service deployment in a container for verification purposes in this article.

[Unit]
Description=Python web demo
After=network.target

[Service]
User=webapp
WorkingDirectory=/srv/apps/web-demo
EnvironmentFile=/etc/web-demo.env
ExecStart=/srv/apps/web-demo/.venv/bin/uvicorn web_demo.main:app --host 127.0.0.1 --port 8000
Restart=on-failure

[Install]
WantedBy=multi-user.target

systemd does not automatically execute source .venv/bin/activate like an interactive shell. Please specify the execution path, working directory, and user permissions of the service. When you re-run uv run in the usual options in the operating environment, the default dev group may be included, so set the synchronization policy for operation and the execution policy together.

When frontend and workers increase

When the browser frontend is added, place the Node environment and lock file separately in frontend/. If OCR or GPU model work becomes a deployment unit independent of the API, separate Python specifications and lock files can be placed. Simply placing files in different folders is different from separating the process/dependency environment. The following 05 data·AI and 06 OCR·service separation specify this standard.

Download series example file·venv·conda specification, uv·web·data·OCR example source, and confirmed uv lock file are bundled. It is executed from each project directory, and the OS package is prepared separately through the procedures in the main text.