Making and Deploying an AI Web App in 2023 (Part 3)

Setup the Python Project with Modern Tools

This is part of a multi-part blogpost about how to build an AI Web App. Please refer to Part 1 for more context.

In this post we will have a look at how to setup Python projects using modern tools in the Python ecosystem. This post is all about the tools, and there’s basically no code.

The tools recommended here are mostly very new, and in my opinion, better than the older alternatives. If you haven’t yet tried these tools but are familiar with the alternatives, I’d suggest you give them a go: all of these tools had basically 0 friction while integrating with my workflows.

Project Manager

Alternatives to hatch: Poetry, PDM, Conda, venv.

My currently favorite project manager is hatch. If you’ve never used a project manager before, its main job is keeping track of your dependencies, making sure there are no incompatibilities between libraries, and keeping your environment clean of unnecessary clutter. Project managers also allow you to build or publish your own Python packages with a single command.

I like hatch in particular because it’s super fast at resolving dependencies (unlike Poetry [1] [2]) and has a utility for running scripts and an environment manager. These last 2 features allow hatch to replace tox for many use cases.

At the moment hatch has a big downside compared to Poetry, in that it can’t pin dependencies. You can specify the library versions you want, but their dependencies’ versions won’t be fixed. (See their FAQ about libraries vs applications)

Hopefully this will be solved in the future.

So the first step is to install hatch

pip install hatch

We can then start a new project by running the following command and giving a project name and description

hatch new -i

This command will create a new directory and populate it with an empty template

├── ai_web_app
│   ├──
│   └──
├── tests
│   └──
├── LICENSE.txt
└── pyproject.toml

The most interesting file here is pyproject.toml, where you will keep all your project’s configurations.


Alternatives to ruff: flake8, FlakeHeaven, FlakeHell.

I’m not aware of good alternatives to black or isort.

For linting, my favorite tool is ruff. It’s significantly faster than any other linter, and very simple to setup and forget.

Another great tool for keeping the style the same throughout the code is black. It’s an opinionated code formatter. I love it because I can just set it up in my IDE to run whenever I save a file, and then I don’t have to worry about style or formatting when writing the code. If you’re collaborating on the same code with someone else, black is really indispensable: it forces your team to keep the same style throughout the code, and it makes the git differences much simpler.

The final tool here is isort. This is a very simple tool, which forces imports to have a specific order. That’s it! That also makes the git diffs simpler, and is simple to setup and forget.

To install all of these tools, you just need to edit the pyproject.toml file. Look for the dev dependencies section (it’s the default env in hatch), and just add them there. At this point, your dev dependencies section should look like this:

dependencies = [

isort needs a bit of configuration to play nice with black (see their compatibility guide). Just add the following to the end of pyproject.toml:

profile = "black"

The final step in this section is to make use of hatch and its scripting features to run our linting. Look for the scripts section in the pyproject.toml file ([tool.hatch.envs.default.scripts]) and these 2 new lines (the first lines should be there if you used hatch init):

cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=ai_web_app --cov=tests {args}"
no-cov = "cov --no-cov {args}"
lint = ["ruff .", "black . --check -q", "isort . --check -q"]
format = ["black .", "isort ."]

Pre-commit hooks

Another essential tool, especially if you work in a team: pre-commit git hooks. These are scripts that will be executed before every commit you make, to make sure you don’t make a stupid mistake and commit it into the repo. You can enforce linting in this step, and make it automatic.

For this you need the pre-commit Python package (just add it to your dev environment, like above), and a config file. You can just take my configuration, which uses the tools mentioned above. Create a .pre-commit-config.yaml file and paste this:

  - repo:
    rev: v2.3.0
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
  - repo:
    rev: 5.11.2
      - id: isort
        name: isort (python)
  - repo:
    rev: 22.12.0
      - id: black
  - repo:
    # Ruff version.
    rev: "v0.0.211"
      - id: ruff
        # Respect `exclude` and `extend-exclude` settings.
        args: ["--force-exclude"]

Finally, run

hatch run pre-commit install

Your git hooks will be installed, and all those listed tools will run for every commit that you make.

Type Hinting

Python now supports type hinting, and I’ve been trying to write almost all of my code using it. If you’re using an IDE, it will often alert you of simple mistakes you make, or edge cases you didn’t consider.

However, unfortunately the whole ecosystem isn’t great. Ideally every library you use would have type hints, and you could run mypy in strict mode. But most libraries don’t have type hints, and then mypy has no way of knowing what the types of your imports are.

All that being said, I would still recommend developing with type hints, and using at least your IDE for warnings. If most of the libraries you’re using support type hints, you should also be running mypy often (maybe even as a pre-commit hook?).

In this particular case, the main library I’m using is txtai and it doesn’t have type hints, so I won’t be using mypy much. However, this is how you would set it up: add mypy to your dev dependencies (like above), and add a new script in pyproject.toml to run it (e.g., types = "mypy ."). Furthermore, you should also make an empty file py.typed (see PEP-561) in your Python package.

touch ai_web_app/py.typed
If you’ve been following these instructions, your code should look like this:

To continue this tutorial, go to Part 4.

For comments or questions, use the Reddit discussion or reach out to me directly via email.