Hello, and welcome to the first lesson of the course. We will build an incident-management API gradually, but we will begin with the part that makes every later step reliable: treating the codebase as an installable Python project rather than a collection of scripts.
This first module establishes professional Python project foundations. Today you will create a small, empty-but-valid package using a src layout, run it in an isolated virtual environment, and describe it with pyproject.toml. The result will be ready for FastAPI code, tests, linting, and CI without requiring import-path workarounds later.
Why an application should still be a package
A Python project is the whole repository: documentation, configuration, tests, source code, and deployment assets. A Python package is the importable code unit inside it.
For this course, use these two related names:
- Distribution/project name:
incident-api - Python import package:
incident_api
The hyphenated project name is conventional for a package installed by pip. Python identifiers cannot contain hyphens, so imports use an underscore:
import incident_api
An installable package is useful even if you never publish it to PyPI. Installation gives Python a deliberate, reproducible way to locate the application code. That prevents a common failure mode in script-grown projects: code works only when a developer runs it from one particular directory, or only after setting PYTHONPATH.
The src layout makes this discipline concrete:
incident-api/
├── .gitignore
├── pyproject.toml
├── README.md
└── src/
└── incident_api/
└── __init__.py
The repository root is deliberately different from the source directory. When you start Python from incident-api/, Python can see that directory, but it does not automatically see incident-api/src/. Therefore, import incident_api succeeds only after the project has been installed into the environment. This is a productive constraint: your local execution and tests exercise the package as installed, much closer to how it will run in CI or a container.

My 2025 uv-based Python Project Layout for Production Apps
In “My 2025 uv-based Python Project Layout for Production Apps,” Hynek Schlawack explains the central reason for the src layout: it prevents local files from accidentally masquerading as an installed package. This short portion is tool-independent; use it for the rationale, then follow the venv workflow below.
Watch the src rationale. Focus on the distinction between a directory that merely happens to contain Python files and a package that is actually installed and imported.
Two practical consequences follow:
- Do not add
srctoPYTHONPATHto “fix” imports. - Do not modify
sys.pathinside application code.
Both can hide a broken package configuration until a deployment or another developer’s machine exposes it.
Create the repository and its isolated environment
Create a new Git repository named incident-api. You can create the directories in your editor or from a terminal. From the repository root, create this source path:
src/incident_api/
Inside src/incident_api/, create __init__.py with only a package docstring:
"""Incident management API package."""
An __init__.py file marks this directory as a regular Python package. Keeping it free of application startup code is important: importing incident_api should not open network connections, load production configuration, or start a server.
Create a minimal README.md at the repository root:
# Incident API
An internal service for managing operational incidents.
Also create .gitignore:
.venv/
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
The virtual environment is local machine state, not source code. Generated metadata such as *.egg-info/ and future distribution artifacts under dist/ should likewise stay out of version control.
The virtual environment
A virtual environment gives this project its own Python interpreter context and its own installed packages. It is analogous to keeping a CI job’s dependencies isolated rather than relying on whatever tools happen to be installed on a shared build agent.
First, verify the Python version you intend to use:
python --version
For this course, use Python 3.12 or later. If multiple interpreters are installed, invoke the desired one explicitly when creating the environment.
| Environment | Create the environment in the project root |
|---|---|
| Linux or macOS | python3.12 -m venv .venv |
| Windows with the Python launcher | py -3.12 -m venv .venv |
Any platform where python is already Python 3.12+ | python -m venv .venv |
The .venv convention makes the environment visible to editors while clearly identifying it as project-local.
venv — Creation of virtual environments
Read the official Python documentation, “venv — Creation of virtual environments,” to understand what Python creates and why a virtual environment should be recreated rather than copied between machines.
In “Creating virtual environments,” read the creation model, noting the environment-specific interpreter and site-packages directory. Then, in “How venvs work,” read the prefix check and the activation table for your shell. Finish with the portability warning: environments are disposable local state and must be recreated at a new path.
Activate the environment for your shell:
| Shell | Activation command |
|---|---|
| Linux/macOS Bash or Zsh | source .venv/bin/activate |
| PowerShell | .venv\Scripts\Activate.ps1 |
| Windows Command Prompt | .venv\Scripts\activate.bat |
After activation, check that the interpreter really is inside the environment:
python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
The first line should point into .venv; the second should print True.
Activation is a shell convenience: it adjusts PATH so that python and installed commands refer to .venv. It is not magic, and it is not required in automation. A CI pipeline can always call the environment’s Python executable directly. For interactive work, activation reduces mistakes.
If PowerShell refuses to run Activate.ps1, it may be subject to an execution policy. Follow your organization’s policy; the Python documentation describes the user-scope RemoteSigned option. Avoid bypassing execution controls blindly.
Describe the project with pyproject.toml
At the repository root, create pyproject.toml:
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "incident-api"
version = "0.1.0"
description = "An internal API for managing operational incidents."
readme = "README.md"
requires-python = ">=3.12"
[tool.setuptools.packages.find]
where = ["src"]
include = ["incident_api*"]
This small file has three separate responsibilities.
1. The build system
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
This tells installation tools how to build your project. pip can create a temporary isolated build environment, install the declared build requirement there, and invoke the named backend.
setuptools is the build backend selected for this course because it is mature, standards-based, and adequate for an application package. It is not an application dependency. FastAPI, database drivers, and test tools will be classified deliberately in the next lessons.
2. Standard project metadata
[project]
name = "incident-api"
version = "0.1.0"
requires-python = ">=3.12"
The [project] table contains metadata defined by the Python packaging standard.
nameis the installed distribution name. Package tools normalize names, so hyphens and underscores are generally treated consistently for installation.versionidentifies this build of your project.0.1.0is a reasonable initial version; the versioning strategy can evolve as the service matures.requires-pythonstates the versions your project supports. It does not install Python or switch your current interpreter, so keep your local.venvaligned with it.readmeanddescriptionmake the package understandable to a future maintainer and, if you later publish it internally, to consumers.
3. Source-package discovery
[tool.setuptools.packages.find]
where = ["src"]
include = ["incident_api*"]
This is setuptools-specific configuration. It says that importable packages should be discovered beneath src, including incident_api and any future subpackages such as incident_api.domain.
For a conventional src layout, setuptools can often discover packages automatically. Declaring where = ["src"] explicitly makes the intended boundary clear and reduces ambiguity when the repository later contains additional directories.
Configuring setuptools using pyproject.toml files
The setuptools guide connects the configuration you just wrote to the PEP 621 packaging standard. Read it to distinguish standard project metadata from build-backend-specific settings.
In the introductory material before “Setuptools-specific configuration,” read the core tables. Compare its [build-system] and [project] tables with yours; do not copy its optional publishing metadata yet. Then go to “Setuptools-specific configuration” and read the Tip beginning the discovery rule, continuing through the find configuration example. Notice that the src layout is a supported conventional layout.
Install your project in editable mode
With .venv activated and your terminal at the directory containing pyproject.toml, install the project:
python -m pip install --editable .
The dot means “this directory.” The --editable option, also written -e, installs a development link to the project rather than freezing a copy of its source into site-packages.
That distinction matters during development:
- Changes to Python files under
src/are immediately used the next time Python imports or runs the package. - Changes to
pyproject.tomlaffect installation metadata and usually require rerunning the same install command. - The project is now importable independently of the terminal’s current directory.
Validate it while still at the project root:
python -c "import incident_api; print(incident_api.__file__)"
python -m pip show incident-api
The first command should print a path ending in:
src/incident_api/__init__.py
Now move to the parent directory and repeat the import:
cd ..
python -c "import incident_api; print(incident_api.__file__)"
It should still work because the package is installed into .venv, not because you happen to be standing next to its source. Return to the repository afterward:
cd incident-api
You may notice a directory such as src/incident_api.egg-info/ after installation. This is generated metadata used by the build and installation tooling. It is expected and is why .gitignore excludes *.egg-info/.
A useful checkpoint is your repository’s state:
incident-api/
├── .gitignore
├── .venv/ # local and ignored
├── README.md
├── pyproject.toml
└── src/
├── incident_api/
│ └── __init__.py
└── incident_api.egg-info/ # generated and ignored after installation
At this point, your project has no runtime dependencies and no web endpoint. That is intentional. It is a correctly installable foundation, and we can now add dependencies without mixing application requirements, development tools, and machine-local state.
Key takeaways
You have established four professional habits:
- The repository is a project;
src/incident_apiis its importable package. - The src layout prevents accidental imports from the checkout and exposes packaging mistakes early.
.venvisolates local installed packages and is disposable; recreate it rather than committing or copying it.pyproject.tomldeclares how the project is built, its standard metadata, supported Python version, and package discovery rules.python -m pip install --editable .makes your source package available in the active environment while you develop it.
In the next lesson, you will add runtime and development dependencies separately and generate a reproducible lock file. That will turn this clean package shell into a controlled environment for FastAPI, testing, formatting, and type checking.
Can't find a good explanation? Sign up and we'll make it for you
Sign up