Empty Python App

Introduction
With the recent increase of development of AI apps, I have started using the Python programming language.
For who doesn't know Python (like myself just a few months ago), Python is very different from other compiled languages like C# and Java.
It is somehow more similar to JavaScript, but even here we have TypeScript that allows compilation, with various degrees of stricness.
The issues
What is the biggest problem of this lack of static code checks in Python: that typically you develop a page, it works, then you develop a second page, you modify some shared code, and you don't realize that the first page has stopped working.
Other examples can simply be wrong types passed to functions... a lot of annoyances that should be found at compile time.
The remedy
Still, there is "something" that can be done in Python to improve code quality, like for example the use of the linter, and of course unit tests.
But when putting all of this in one project, I had so many issues... maybe basic for some expert Python developer, but not for me.
So I have created a template project, with the goal of having all these pieces in place. It is hosted in a public GitHub repo.
In this blog post I will describe what I have done and the rationals behind it.
DevContainers
Introduction
Before dealing into the Python specific topics, my ideal project should support DevContainers.
What are DevContainers? Essentially they allow you to develop inside a Docker container and they offer the following advantages:
- Apart from the basic DevContainer setup (that I will describe later) you don't need to install anything specific to your programming language on your computer. You don't need Python, Node, or any other programming language.
- For the same reason, you have isolation between environments and projects. So you can choose to use Python 3.13, while in another project you use an older version of Pyhton... well, no problem!
- Another advantage: if you screw up the container, no problem, delete it, and recreate the container starting again from a clean setup.
- About the host computer: it can be Windows, MacOS or Linux. You can choose to use what you want, while development will happen inside the Linux container.
- Even Visual Studio Code tailors itself when developing inside a container. You can choose what extensions to install, which settings to use, and so on. Again, working in "my" Python project you can have Visual Studio Code already set up according to the conventions used in my project, while working in another one, you will have different customizations.
My implementation
To use DevContainers, you simply create a .devcontainer folder, that will contain a devcontainer.json file and eventually a Dockerfile.
While theoretically it should be possible to use any Dockerfile as image, it is suggested to use one of the standard images and then add features.
In my case I have started with the Debian image, and then I have added Docker in Docker, Node (specifying also the version v18) and Python (in version 3.13).
Then in the customizations/vscode section, we can specify which extensions we want to install in Visual Studio Code and the settings for these extensions.
We can specify environment variables, in my case PYTHONPATH, and external mounts (in my case, to pass GitHub credentials/keys).
Finally can provide a postCreateCommand, that is invoked after the container is created. In my case I started to have a long list of commands, so I have preferred to list these commands in a separate setup.sh. The commands that I invoke are:
- npm install and first TypeScript build in debug mode
- creation of the Python virtual environment
- pip install
With the exception of the Python virtual environment creation, all other operations are also run before each debug. I run them here because the creation of the container can take some time, and it makes sense to run lengthy operations during container creation.
What is required
What are you required to install on your computer?
- Docker, because of course we use containers
- Visual Studio Code with the Dev Containers extension
That's all!
If you clone my GitHub repo, when you open the folder in Visual Studio Code, it will offer you to open it inside DevContainer.
Choose yes, and Visual Studio will build the container for you.
After that, you will be able to go to the Debug tab and start debugging without doing anything more!
.vscode
In the .vscode folder we have three files:
- settings.json: this file contains settings and customizations for Visual Studio Code. In particular, I prefer to use tabs instead of spaces, and the configuration is in this file.
- launch.json: this file is useful for the integrated debugger. It starts the src/app.py file, but only after running the "Run Python Unit Tests" task.
- tasks.json: file this contains the tasks that I enqueue one after each other. So for example we have the "Run Python Unit Tests" task, that is dependent on the "Run PyLint on 'tests' folder", and so on.
The tasks
The queue of tasks that I have configured is:
- Install NPM dependencies: so that if you add packages to your package.json file, they are automatically installed when you start a debug session.
- Build TypeScript: so that if you modify your TypeScript code, it gets transpiled and you are sure to always debug the latest version.
- Install Python dependencies: so that if you add packages to the requirements.txt file, they are automatically installed.
- Run PyLint on 'src' folder: I want to successfully execute the linter on the src folder, that contains the full application.
- Run PyLint on 'tests' folder: I also want to execute the linter on the unit tests.
- Run Python Unit Tests: as the name suggests, I want to successfully run all the unit tests.
Two important considerations:
- The list of tasks is totally opinable. If for example you don't want to restore all the Node and Python packages, or transpile the TypeScript, or lint the unit tests, no problem: you can simply comment parts of the tasks.json file and adjust dependencies accordingly.
- In case some of these tasks fails, the default behavior is to avoid starting the debug session. But if you want, you can start it anyway (just in case you need to test something quickly and then you will fix what you have left behind).
The project
In this section I want to describe the project.
It's a Python project, with UI in Flask and some JavaScript transpiled from TypeScript.
Flask
Why Flask and not FastAPI or whatever other framework? Because why not.
Flask is still widely used in the Python world.
If you want to use other Python framework or libraries, feel free to do it, remove the references to Flask and add your own library.
This won't change the validity of the solution and the approach I have adopted to ensure quality.
UI in Python and TypeScript
Why have I chosen to develop the UI directly in Python and not expose APIs to Node frontends? In this case, I think that this question makes more sense and I took some time before choosing.
And again, why not? Developing a frontend in Node would have required many other choices (React vs Vue vs whatever other framework) and above all expanded the scope of the POC too much. In the end in JavaScript we have many frameworks that are already able to scaffold project following the best practices.
src folder
All source code is in the src folder.
Both in the DevContainer and in the production Dockerfile, we have a PYTHONPATH environment variable pointing to this path.
The app.js is very minimal, just an excuse to load a html, a css and the transpiled JavaScript.
It references the config.py file, that is used to centralize loading of configuration settings from environment variables or .env file.
On purpose I have not added anything related to AI. The goal is a template Python application, without any constrain relating it to AI.
pylint
As said in the introduction, one of the main goals was to run the linter before debugging (as we don't have a real compilation).
For the linter to work correctly, it needs to load the application source codes. It's required that the PYTHONPATH environment variable is set correctly.
It has been my choice to lint both the project (in the src folder) and the unit tests (in the tests folder).
When I need to execute linting manually, from the terminal, I run the following commands:
- pylint src
- pylint tests
Both commands must be executed from the root folder.
pylintrc
The linter configuration is done via the .pylintsrc file.
It contains already some rules that make the current project to lint perfectly.
Feel free to adjust it according to your specific project needs and conventions.
Unit Tests
Unit tests are as important as the linter. Again, I wanted to run them before every debug session.
You can run unit tests in two ways:
- python -m unittest
- python -m unittest discover -s tests
Note that both commands must be executed from the root folder.
TypeScript
Regarding client side code, I wanted to enforce the use of TypeScript and support it as much as possible in my pipeline.
- the client side code is in src/static/src folder. Not extremely nice in my opinion, I have tried other solution, but they had more disadvantages than other (for example, having a typescript folder at root level). The entry point must be main.ts.
- we have a webpack.config.js file that defines the entry point and the target file, that is bundle.js located in src/static/dist.
- the tranpilation parameters are specified in the tsconfig.json file.
- the package.json file defines two commands:
- npm run debug for debug builds
This command is invoked during DevContainer creation and before each debug session. - npm run build for production builds
This command is invoked during production container creation.
- npm run debug for debug builds
Dockerfile
The Dockerfile is not the same container used during development. It is more optimized and it doesn't include the development libraries and tools.
In particular, it is structured in two stages:
- the first stage creates a Node container used to transpile the TypeScript into JavaScript. After this operation, the container is not used anymore
- the second stage creates the target Python container and starts the web server.
The Dockerfile has been written favoring build optimization in favor of readability. In practice, some operations have been changed of order, to put on top the instructions that should change less, and so reduce rebuild times.
Still I have added comments and I think that it is pretty straightforward to understand.
Other
I have a cleanall.sh file used to clean all the temporary file.
It can be run both inside the DevContainer and outside (for example, before committing to GitHub).
And finally, a big thank you to my colleague Aymen Furter for his precious comments and suggestions!