Starting a new Python project doesn't have to be a challenge because the basic needs are always the same even for different types of projects. This article presents how to create a perfect initial base that can be used for any Python project.
Definition of the Perfect Initial Base
The foundation of a perfect Python project should:
- Have a suitable file and directory structure to organize the entire project, separating application code, testing, documentation, and project configuration.
- Use virtual environments to develop the project in isolation, with no outside interference.
- Use linting tools for static code analysis to identify defects, formatting, optimization, security issues, etc. at an early stage of development
- Use automated tests with reports on test coverage.
- Use continuous integration to check code quality as it arrives on the server.
- Use a properly adjusted version control to ignore files that should not be versioned.
Except for the file and directory structure which is unique, all other items depend on choices. And there are many options. Virtual environment management, for example, can be done with
conda. There are dozens of linting tools such as
mypy, etc., that are equivalent or complementary. In the end, the choices that form one or another combination depend on technical and personal decisions.
Virtual Environment Management
The management of Python versions, virtual environments and dependencies will be done through the combination of
pyenv + poetry (read the previous article).
Initial Directory Structure
To create the initial structure of your project, use
poetry new <project_name>:
The previous command creates the following directory structure:
project_x ├── project_x │ └── __init__.py ├── pyproject.toml ├── README.rst └── tests ├── __init__.py └── test_project_x.py
This is an excellent minimum file and directory structure. It separates the project-specific code in the
project_x subdirectory of the code only related to tests in the
tests directory, and the project's configuration and documentation files (
README.rst). However, some adjustments are needed:
README.rstcomes empty, and you need to complete it. Creating this type of file is beyond the scope of this article, but you can find good tips and more information in 1 and 2.
pyproject.tomland change the settings created automatically for
- Check the Python version specified in section
poetry newuses the environment version, but you can install and specify other Python versions via pyenv.
Linting and Testing Tools
The recommended minimum set of testing tools is:
For linting, I recommend using:
- blue: code formatter based on black
- flake8: Python static code analysis tool
flake8plugin to check for forgotten debug commands
flake8plugin to check common style issues or inconsistencies with pytest-based tests.
- isort: tool to sort Python imports.
- mypy: static type analysis tool.
flake8plugin that checks against PEP-8 naming conventions.
- pyupgrade: tool to automatically upgrade syntax for newer versions of the Python language.
And some additional security-specific tools:
- bandit: tool for finding common security holes in Python code.
- pip-audit: tool for scanning Python environments for packages with known vulnerabilities.
Installation and Configuration
All libraries and tools related to testing and linting are necessary for the development of the project, but not for its operation in production. They should be installed in a separate section in
pyproject.toml to not get mixed up with the essential dependencies. To install them, use
poetry add --dev:
We can keep most dependencies configurations in
pyproject.toml, in sections named following the pattern
PEP8 establishes a style guide for Python code but leaves room for some formatting variations. By setting
profile = "black"on line 2, isort formatting is adjusted to be more compatible with the one produced by black/blue.
- Lines 3 and 6 change the line size from the default value of
mypyhas several config options.
ignore_missing_importssuppresses error messages about imports that cannot be resolved (line 12).
disallow_untyped_defsdisallows defining functions without type annotations or with incomplete type annotations (line 13).
Unlike the other tools adopted,
flake8 cannot be configured in
pyproject.toml. We should use a file named
Testing and linting should be easy to run without remembering each command and its arguments. For this, I recommend using a
Makefile with the necessary tasks:
test: pytest --cov-report term-missing --cov-report html --cov-branch \ --cov project_x/ lint: @echo isort --diff -c . @echo blue --check --diff --color . @echo flake8 . @echo mypy . @echo bandit -r project_x/ @echo pip-audit format: isort . blue . pyupgrade --py310-plus **/*.py
And then, just use
make <task> :
make testruns the tests and generates test coverage reports.
make lintruns several linting tools in sequence.
make formatformats Python code according to the patterns used by
We can use these same commands in version control hooks and in the continuous integration configuration.
Continuous Integration System Configuration
Most modern continuous integration systems keep their configuration in the source code. GitHub Actions, for example, keep your configuration in
yaml files, inside the
.github/workflows directory. For our project, we are going to use
This configuration works as follows:
- This workflow will be executed every time the repository receives a
- The stream will run on an Ubuntu operating system in the latest available version (line 5).
- Use Python version
- Next, install
poetry(line 16) and configure it to use virtual environments in
.venvdirectories (line 19).
- Create a
cachepolicy for the
.venvdirectory (line 25). The key that identifies the
cacheis formed by the concatenation of the word
poetry.lockhash (line 26).
- Dependencies are installed only if the
cacheis not found (lines 28 to 30).
- Run the lint task (line 33)
- Run the test task (line 35)
It is good practice to do the code quality check locally, even if continuous integration does the same process on the server again. It saves time because the result is immediate, and corrections can be made outside a continuous integration cycle.
This local check will be automated via version control hooks to trigger actions according to some event. We will need two events:
In both cases, the version control operation is cancelled if there are any failures.
To make the developer's life easier, let's add a
install_hooks task to the
Makefile, which calls the
scripts/install_hooks.sh to create the hooks:
And this is
- On Git, hooks are executable files named according to the desired event, located in
- On Mercurial, hooks are defined in the [hooks] section of the .hg/hgrc configuration file, where each hook can be Python commands or functions.
- Both scripts in
bash(lines 3-6, 8-11) as the commands used for Mercurial (lines 13-16) do the same thing: change the current directory to the root of the project, where
Makefileis located, and run the command
poetry run make <task>. Remember that
poetry run <command>runs the command within the context of the project's virtual environment.
- If a
.gitdirectory exists, so the Git hooks are created in the
.git/hooksdirectory (lines 18-21). Otherwise, Mercurial hooks are created in the
.hg/hgrcdirectory (lines 22-23).
- The code snippet presented serves those who use Mercurial (my case) and those who use Git. You can remove some parts if only need one or the other.
Preparing Version Control
To prevent unwanted files from being mistakenly added to version control, you need to create a filter list in a particular file located at the root of the project, at the same level as the
.git directory depending on which tool you use. If you use Mercurial, this file should be called
.hgignore and should contain:
syntax: glob .venv .env *~ *.py[cod] *.orig # Unit test / coverage reports .coverage htmlcov/ # cache __pycache__ .mypy_cache .pytest_cache
If you use
Git, the filename must be called
.gitignore and contain the same lines as above minus the first line (
syntax: glob), which should be removed.
With the filters defined, we can start version control. For Mercurial, the commands are:
If you use Git, execute:
$ git init . $ poetry run make install_hooks $ git add -A . $ git commit -m 'Initial project structure'
And everything is ready to upload to the official project repository on GitHub.
Ready-to-Use Template on GitHub
It is important to know all the steps to create the perfect Python project. But instead of executing these same steps at each new project, you can just use the template that I made available on Github. Instructions for use are in
The design basis presented in this article works very well and can be easily adapted to other tools, if you want to try a different combination. The most important thing is to maintain the project structure and linting and testing activities automated.
|1||Make a README|
|2||READMEs on READMEs (and other README-related resources)|
|3||How to set up a perfect Python project|