New Python Tools That Deserve Your Attention

New Python Tools That Deserve Your Attention

Things seem to be accelerating. That means I'm either getting older, or that there are more people on the planet and they're coding. Here are some new tools that I've decided to keep using — and there's a twist.

pyproject.toml

I've moved on from setup.py, advancing through setup.cfg, to finally embrace pyproject.toml. With Python 3.12, the standard library now includes a TOML package, making configuration data accessible effortlessly during development and testing.

This has allowed me to extend my Makefile:

setup:
    if which uv && [ ! -d .venv ] ; then uv venv ; fi
    source .venv/bin/activate \
        && uv pip install -q -U pip \
        && uv pip install nodeenv invoke ruff \
        && nodeenv -p \
        && npm install -g prettier \
        && uv pip install -e '.[dev]' \
        && cd src/client && npm install

This now provides me with a virtual environment containing both python and node.js — the perfect way to use tornado and vite.js. I install prettier to perform a similar job to ruff for javascript.

uv

I love virtual environments — welcome uv. This is the fastest way to create a virtual environment — the default created is '.venv', which is the same as used by vs code. The syntax calling pip is uv pip install <package-name>. uv is written in rust and is blazingly fast. Both the creation of the venv and pip installs are so much faster. This one is a keeper and I look forward to its expansion. Of course, uv itself has to be installed somewhere. You can either user pip to install it in your base python or something like homebrew.

I've also used uv in my docker builds and it makes everything smaller and faster.

ruff

I dislike formatting by hand and thanks to linters we no longer have to. I use invoke and have this run before I commit:

@task
def lint(ctx):
    """ run axblack and pylint """
    ctx.run(f'isort {PROJECT_NAME}', pty=True)
    ctx.run(f'black {PROJECT_NAME} tests', pty=True)
    ctx.run(f'pylint {PROJECT_NAME}', pty=True)

I used to install axblack to achieve single quotes, with double quotes for comments. So readable, works a charm.

Now, to achieve the same result, I use:

@task
def lint(ctx):
    """format and check"""
    ctx.run("ruff format", pty=True)
    ctx.run("ruff check --select I --fix", pty=True)

In my pyproject.toml, I include the following configuration settings:

[tool.ruff]
line-length = 79
include = ["pyproject.toml", "src/**/*.py"]

[tool.ruff.format]
quote-style = "single"

The speed difference is phenomenal!

So, now I have a compact src directory project with both client and server hot reloadable, and an invoke cli to remember all the functions I call during development. For example:

@task
def run(ctx, debug=False):
    """run our server and in debug, too"""
    cmd = f'python3 -m {PROJECT}.web.main'
    if debug:
        cmd += '--debug & cd src/client && npm run dev'
    ctx.run(cmd, pty=True)

Zed

Yes, I've skipped town and started using Zed. It does everything I want an editor to do — I loved the now sunsetted Atom. I know VS Code does everything. I still use it to debug and utilize its many extensions. But I prefer the speed and simplicity of Zed. I also use its ai assistants. It's fast because they developed it in Rust.

Zed uses the Pyright language server, which uses the following configuration in my pyproject.toml:

[tool.pyright]
exclude = ["**/node_modules", "**/__pycache__"]
include = ["src"]
venvPath = "."
venv = ".venv"
executionEnvironments = [{ root = "src" }]

Anyone noticing the elephant in the room? No, I'm not referring to Postgres, but we could touch on that.

Postgres

I've been using Postgres. I used to use MySQL. Then I discovered async sqlite. Given my use of SqlAlchemy, which backend should not matter. But I had a look through the documentation for psycopg and found that it provides a row_factory setting on its cursor. This allows you to bind tuples, namedtuples, dicts and dataclasses to the result of your sql query. The fastest options are tuple and namedtuple - these are read only - but they are not much faster than using SqlAlchemy's expression language. Migrations with Alembic are also a bonus - it truly is possible to declare something once in python and have your database schema reflect that.

I have used a couple of Postgres services and found that neon.tech is the fastest. It seemed to out perform heroku and koyeb.

One of the examples of bun.sh uses javascript tagged templates:

import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL)
const rows = await sql`SELECT version()`
console.log(rows[0].version)

Do you see that use of a sql tag? That you cannot do in Python. The closest would be to subclass str and override the format method to sql quote all *arg values and **kwarg values and the add an async def call function. Hey Python! Can we have tagged templates?

Back to the elephant…

Rust

No, I am not using Rust. I'm still devoted to Python and I anticipate even more improvements when they dispose of the GIL in 3.13 or make multi-interpreters available for multi-processing. But all these tools, bar Postgres, are developed in Rust and are astoundingly speedy. I read one commentator saying they occasionally put an error in their code to be sure that ruff was actually doing its job!

I know I will learn Rust. I tried to replace Python with Node. I've even had a week with bun.sh. There are some wonderful things in javascript land, but not enough to make me emigrate.

Rust is making Python easier to use with speedier tools. Anyone, younger, reading this should take this seriously. Tool makers are building things in Rust to make Python more usable. Why? Is this not extraordinary? Should we all be writing in Rust?

AI is still not ready to code. Oh, it's brilliant, don't get me wrong. But responding is not the same as asking the question in the first place. If it's going to help, ask it to help you with Rust?

I — as distinct from AI — will keep writing about Python…