Simple Python App Setup 👨💻</>
Introduction
Many tutorials show you how to create a basic Python app with a couple of modules, but they rarely explain how to set up a whole project.
This guide will show you how to set up a simple Python project with automated testing and a virtual environment for 3rd party packages. I have based it on Dead Simple Python: Project Structure and Imports by Jason C. McDonald with some opinionated changes.
This guide has been tested on GNU/Linux.
Setup
Download Python
First, you need to install Python. As I am using Arch Linux as my base, you can use the pacman package manager to install Python.
sudo pacman -S install python3
Then, ensure it’s installed by running:
python3 --version
Create a Python Project
Create a new directory for your project and cd
into it (e.g., calculator
, myapp
, etc.).
mkdir sample
cd sample
Create a directory for your Python code, giving it the same name as your project to create a top-level package. Avoid using hyphens in the name (e.g., sample-project
is not acceptable). Instead, use a single word (e.g., sample
) or underscores (e.g., sample_project
) if they improve readability.
mkdir sample
Create a bunch of Python modules in the top-level package.
touch sample/__init__.py
touch sample/__main__.py
touch sample/app.py
You should have the following directory structure.
[~/sample] $ tree
.
└── sample
├── __init__.py
├── __main__.py
└── app.py
Add the following to sample/app.py
.
def run() -> None:
print("Hello World!")
Add the following to sample/__main__.py
.
from sample import app
def main() -> None:
app.run()
if __name__ == "__main__":
main()
Run the app.
python3 -m sample
Hello World!
Now let’s go step by step through what you did.
You created a top-level package in the root of your project. It has the same name as your project (e.g.,
~/sample/sample
).mkdir sample
You created a bunch of Python modules in the top-level package directory. Each package must contain an
__init__.py
. The__main__.py
serves as the entry point to the app andapp.py
houses the actual app’s logic. While you could technically put everything fromapp.py
inside__main__.py
, I prefer to keep__main__.py
small, much like in my C++ projects. That is to say,app.py
isn’t mandatory, but it helps keep__main__.py
tidy.touch sample/__init__.py touch sample/__main__.py touch sample/app.py
In
sample/app.py
, you defined arun()
function that printsHello World!
.def run() -> None: print("Hello World!")
In
sample/__main__.py
, you used absolute imports to import theapp
module from the top-levelsample
package. You then defined amain()
function. This function executes when the script is ran directly usingpython3 -m sample
.from sample import app def main() -> None: app.run() if __name__ == "__main__": main()
Create Subpackages
You can create subpackages in the top-level package directory. For example, you can create a core
(or common
) package that contains the core logic of your app (imported often by various modules), an io
package that contains input/output logic (e.g., reading from disk), a tests
package that contains tests (optional), and a utils
package that contains utility functions (e.g., normalizing and tokenizing a string).
Create a bunch of subpackages in the top-level package directory.
mkdir sample/core
mkdir sample/io
mkdir sample/tests
mkdir sample/utils
Add __init__.py
to each of the subpackages.
touch sample/core/__init__.py
touch sample/io/__init__.py
touch sample/tests/__init__.py
touch sample/utils/__init__.py
Create two modules in the core
and io
subpackages: config.py
and disk.py
.
touch sample/core/config.py
touch sample/io/disk.py
You should have the following directory structure.
[~/sample] $ tree
.
└── sample
├── __init__.py
├── __main__.py
├── app.py
├── core
│ ├── __init__.py
│ └── config.py
├── io
│ ├── __init__.py
│ └── disk.py
├── tests
│ └── __init__.py
└── utils
└── __init__.py
Add the following to sample/core/config.py
.
verbose: bool = True
Add the following to sample/io/disk.py
.
from sample.core import config
def write_to_disk(
file_path: str,
data: str,
) -> None:
with open(file_path, mode="w", encoding="utf-8") as f:
if config.verbose:
print(f"Writing data to '{file_path}'")
f.write(data)
Modify sample/app.py
to use the write_to_disk()
function.
from sample.io.disk import write_to_disk
def run() -> None:
write_to_disk("output.txt", "Hello World!\n")
Now if you run the app, an output.txt
file will be created with the content Hello World!
in your CWD. Since the global verbose
variable is set to True
, the function will print a message to the console.
python3 -m sample
Writing data to 'output.txt'
Now let’s go step by step through what you did.
You created a bunch of subpackages in the top-level package directory. Every package must contain an
__init__.py
file.mkdir sample/core mkdir sample/io mkdir sample/tests mkdir sample/utils touch sample/core/__init__.py touch sample/io/__init__.py touch sample/tests/__init__.py touch sample/utils/__init__.py
You created two modules in the
core
andio
subpackages.touch sample/core/config.py touch sample/io/disk.py
In
sample/core/config.py
, you defined averbose
variable that can be set toTrue
orFalse
to control the verbosity of the program. This module contains global variables that can be read and modified by the program.verbose: bool = True
In
sample/io/disk.py
, you used absolute imports to import theconfig
module from thecore
subpackage. You then defined awrite_to_disk()
function. This function writes data to a file on disk. If theconfig.verbose
variable is set toTrue
, the function prints a message to the console.from sample.core import config def write_to_disk( file_path: str, data: str, ) -> None: with open(file_path, mode="w", encoding="utf-8") as f: if config.verbose: print(f"Writing data to '{file_path}'") f.write(data)
Create Tests
You can create unit tests for your app. First, let’s create something to test.
Create a sample/utils/case.py
module.
touch sample/utils/case.py
Add the following to sample/utils/case.py
.
def lower(
text: str,
) -> str:
return text.lower()
Create a sample/tests/test_case.py
module. The file name must start with test_
to be discovered by the test runner (which you will see later).
touch sample/tests/test_case.py
Add the following to sample/tests/test_case.py
. You should have a test (beginning with test_
) for each function in the case.py
module. The test methods must be independent of each other and you can have multiple assert methods within a single test method.
import unittest
from sample.utils import case
class TestCase(unittest.TestCase):
def test_lower(
self,
) -> None:
self.assertEqual(case.lower("HELLO"), "hello")
self.assertEqual(case.lower("WORLD"), "world")
self.assertEqual(case.lower("GitHub"), "github")
if __name__ == "__main__":
unittest.main()
You should have the following directory structure.
[~/sample] $ tree
.
├── output.txt
└── sample
├── __init__.py
├── __main__.py
├── app.py
├── core
│ ├── __init__.py
│ └── config.py
├── io
│ ├── __init__.py
│ └── disk.py
├── tests
│ ├── __init__.py
│ └── test_case.py
└── utils
├── __init__.py
└── case.py
Now you can run the tests. This will run all the tests in the sample/tests
package.
python3 -m unittest discover sample.tests
Create Virtual Environment
If you use any third-party libraries, you should create a virtual environment to avoid conflicts with other projects. It might seem inconvenient at first, but you’ll get used to it. Do not install third-party libraries globally.
Create a new directory for your virtual environments.
mkdir -p ~/.local/env/
Create a Python virtual environment for the project. The name of the virtual environment should be the same as the name of the project.
python3 -m venv ~/.local/env/sample
Activate the virtual environment. You have to do this every time you open a new terminal. In VSCode, you can set the Python interpreter manually to ~/.local/env/sample/bin/python3
and then enable python.terminal.activateEnvInCurrentTerminal
to do this automatically.
. ~/.local/env/sample/bin/activate
Create a requirements.txt
file with the required dependencies in the root of your project.
touch requirements.txt
Add the following to requirements.txt
.
loguru
Install the required dependencies from the requirements.txt
file. You can pip3 install
the dependencies yourself but the point of requirements.txt
is to make it easier for others to install the dependencies required for your project.
pip3 install -r requirements.txt
Modify sample/__main__.py
to use the loguru
for logging.
from loguru import logger
from sample import app
def main() -> None:
logger.info("Running app")
app.run()
logger.success("App ran successfully")
if __name__ == "__main__":
main()
Now if you run the app, you will see log messages in the console.
python3 -m sample
2024-04-23 19:12:41.410 | INFO | sample.__main__:main:7 - Running app
Writing data to 'output.txt'
2024-04-23 19:12:41.411 | SUCCESS | sample.__main__:main:9 - App ran successfully
That’s it.
Final Thoughts
This guide should give you a basic understanding of how to set up a simple Python project with automated testing and a virtual environment for 3rd party packages.