Do you want your team to enjoy your development workflow? Do you think building software should be fun and existentially fulfilling? If so, this is the post for you!
I’ve been developing with Django for years, and I’ve never been happier with my Django project set up than I am right now. Here’s how I’m making a day of developing with Django the most relaxing and enjoyable development experience possible for myself and my engineering team.
A custom CLI tool for your Django project
Instead of typing:
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py collectstatic
python3 manage.py runserver
Wouldn’t it be much nicer to type:
make start
…and have all that happen for you? I think so!
We can do that with a self-documenting Makefile! Here’s one I frequently use when developing my Django applications, like ApplyByAPI.com:
VENV := env
BIN := $(VENV)/bin
PYTHON := $(BIN)/python
SHELL := /bin/bash
include .env
.PHONY: help
help: ## Show this help
@egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY: venv
venv: ## Make a new virtual environment
python3 -m venv $(VENV) && source $(BIN)/activate
.PHONY: install
install: venv ## Make venv and install requirements
$(BIN)/pip install --upgrade -r requirements.txt
freeze: ## Pin current dependencies
$(BIN)/pip freeze > requirements.txt
migrate: ## Make and run migrations
$(PYTHON) manage.py makemigrations
$(PYTHON) manage.py migrate
db-up: ## Pull and start the Docker Postgres container in the background
docker pull postgres
docker-compose up -d
db-shell: ## Access the Postgres Docker database interactively with psql. Pass in DBNAME=<name>.
docker exec -it container_name psql -d $(DBNAME)
.PHONY: test
test: ## Run tests
$(PYTHON) manage.py test application --verbosity=0 --parallel --failfast
.PHONY: run
run: ## Run the Django server
$(PYTHON) manage.py runserver
start: install migrate run ## Install requirements, apply migrations, then start development server
You’ll notice the presence of the line include .env
above. This ensures make
has access to environment variables stored in a file called .env
. This allows Make to utilize these variables in its commands, for example, the name of my virtual environment, or to pass in $(DBNAME)
to psql
.
What’s with that weird “##
” comment syntax? A Makefile like this gives you a handy suite of command-line aliases you can check in to your Django project. It’s very useful so long as you’re able to remember what all those aliases are.
The help
command above, which runs by default, prints a helpful list of available commands when you run make
or make help
:
help Show this help
venv Make a new virtual environment
install Make venv and install requirements
migrate Make and run migrations
db-up Pull and start the Docker Postgres container in the background
db-shell Access the Postgres Docker database interactively with psql
test Run tests
run Run the Django server
start Install requirements, apply migrations, then start development server
All the usual Django commands are covered, and we’ve got a test
command that runs our tests with the options we prefer. Brilliant.
You can read my full post about self-documenting Makefiles here, which also includes an example Makefile using pipenv
.
Save your brainpower with pre-commit hooks
I previously wrote about some technical ergonomics that can make it a lot easier for teams to develop great software.
One area that’s a no-brainer is using pre-commit hooks to lint code prior to checking it in. This helps to ensure the quality of the code your developers check in, but most importantly, ensures that no one on your team is spending time trying to remember if it should be single or double quotes or where to put a line break.
The confusingly-named pre-commit framework is an otherwise fantastic way to keep hooks (which are not included in cloned repositories) consistent across local environments.
Here is my configuration file, .pre-commit-config.yaml
, for my Django projects:
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: detect-aws-credentials
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
- repo: https://github.com/asottile/blacken-docs
rev: v1.7.0
hooks:
- id: blacken-docs
additional_dependencies: [black==19.3b0]
- repo: local
hooks:
- id: markdownlint
name: markdownlint
description: "Lint Markdown files"
entry: markdownlint '**/*.md' --fix --ignore node_modules --config "./.markdownlint.json"
language: node
types: [markdown]
These hooks check for accidental secret commits, format Python files using Black, format Python snippets in Markdown files using blacken-docs
, and lint Markdown files as well. To install them, just type pre-commit install
.
There are likely even more useful hooks available for your particular use case: see supported hooks to explore.
Useful gitignores
An underappreciated way to improve your team’s daily development experience is to make sure your project uses a well-rounded .gitignore
file. It can help prevent files containing secrets from being committed, and can additionally save developers hours of tedium by ensuring you’re never sifting through a git diff
of generated files.
To efficiently create a gitignore for Python and Django projects, Toptal’s gitignore.io can be a nice resource for generating a robust .gitignore
file.
I still recommend examining the generated results yourself to ensure that ignored files suit your use case, and that nothing you want ignored is commented out.
Continuous testing with GitHub Actions
If your team works on GitHub, setting up a testing process with Actions is low-hanging fruit.
Tests that run in a consistent environment on every pull request can help eliminate “works on my machine” conundrums, as well as ensure no one’s sitting around waiting for a test to run locally.
A hosted CI environment like GitHub Actions can also help when running integration tests that require using managed services resources. You can use encrypted secrets in a repository to grant the Actions runner access to resources in a testing environment, without worrying about creating testing resources and access keys for each of your developers to use.
I’ve written on many occasions about setting up Actions workflows, including using one to run your Makefile, and how to integrate GitHub event data. GitHub even interviewed me about Actions once.
For Django projects, here’s a GitHub Actions workflow that runs tests with a consistent Python version whenever someone opens a pull request in the repository.
name: Run Django tests
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install dependencies
run: make install
- name: Run tests
run: make test
For the installation and test commands, I’ve simply utilized the Makefile that’s been checked in to the repository. A benefit of using your Makefile commands in your CI test workflows is that you only need to keep them updated in one place – your Makefile! No more “why is this working locally but not in CI??!?” headaches.
If you want to step up your security game, you can add Django Security Check as an Action too.
Set up your Django project for success
Want to help keep your development team happy? Set them up for success with these best practices for Django development. Remember, an ounce of brainpower is worth a pound of software!
django python leadership coding docs