The release of version 2.0.0 of schablone , a cookiecutter starter template, makes everything even simpler.

First things first, this is how it looks like:

Schablone landing page screenshot Schablone app screenshot

The seasoned developer immediately recognizes TailwindUI . It’s a nice way to get started with ready made components. Make use of it while people haven’t yet realized why so many websites look similar . And once they do, thanks to Tailwind it’s a breeze to customize the look and feel.

Why is this Django starter template simple?

schablone comes with following features out-of-the box:

Design decisions

Following design decisions make schablone simple. Every design decision briefly discusses its downsides.

Tailwind without Node

Tailwind is great, Node is great. It’s also great to avoid installing Node and NPM next to Python just to compile CSS.

With schablone you can run make run and Tailwind will be installed in the background using a self-contained CLI. No Node needed.

/Library/Developer/CommandLineTools/usr/bin/make -j2 watch.css watch.server
tailwindcss --watch -i foobar/static/css/project.css -o foobar/static/css/dist/styles.css
python manage.py runserver
Downloading 'tailwindcss-macos-arm64' from 'https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64'...

Once Tailwind is installed, it compiles and watches the CSS creating the file static/css/dist/styles.css, which triggers a restart of the dev server.

Hit Ctrl+R for magic. 🪄

Downside: Only official Tailwind plugins are supported such as @tailwindcss/forms and @tailwindcss/typography . If you need anything else you have to install Node.

Traditional user authentication requires users to sign up with email addresses and passwords. In the Django world, django-allauth covers all your user registration, login and OAuth needs.

There are 3 reasons why schablone uses magic links. Magic links are links that are sent out to users which they can use to log in.

  1. Using passwords is hard: Even with strict password policies, it is hard to ensure that users are using passwords in a safe way. Strong passwords can be shared across apps and it takes just one bad apple 🍎 storing passwords in plain text to get pwnd .

  2. Magic links reduce friction: The login form contains one input field for the email address. There is no need to either a) come up with a secure password or b) pull up the password manager and generate one. One thing less to take care of as a user that just wants to use your service.

  3. Login and registration are the same thing: schablone implements a magic link flow, where a user is created upon login if the email address was not found. The login form and the registration form are the same thing. For you, there is less stuff to maintain and less things that can break.

Schablone login page screenshot

Schablone confirmation page screenshot

This is the Python code that either retrieves a user or creates one.

def get_or_create_user(self, email: str) -> "User":
    """Find or create a user with this email address."""
    User = get_user_model()
    user = User.objects.filter(email=email).first()
    if user is None:
        user = User.objects.create(email=email, username=email)
        # user.set_unusable_password()  # type: ignore
    return user

def create_link(self, user: "User") -> str:
    """Create a login link for this user."""
    link = reverse("users:login")
    link = self.request.build_absolute_uri(link)
    link += get_query_string(user)
    return link

Downside: Using magic links for user authentication comes with a heap of security implications that you should study and understand. Magic links might not be the right tool for some use cases and the choice of authentication method should not be informed only by user convenience.

Boring is simple

The main goal of this starter kit is simplicity. That’s why it uses Boring Technology like Django’s templating language (DTL), HTMX and Celery.

You will agree that Django’s templating language is boring. I wrote about how to render forms without django-crispy-forms. The generic form renderer using DTL looks pretty much like this:

{% load widget_tweaks %}
<div class="grid grid-cols-1 gap-6">
  {% for field in form %}
    <label class="block">
      <span class="text-gray-700">{{ field.label }}</span>
      <!-- Checkbox -->
      {% if field|field_type == 'booleanfield' %}
        {{ field|add_class:"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50" }}
        <!-- Date -->
      {% elif field|field_type == 'datefield' %}
        {{ field|add_class:"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" }}
        <!-- Email -->
      {% elif field|field_type == 'emailfield' %}
        {{ field|add_class:"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"|attr:"placeholder:" }}
        <!-- Password -->
      {% elif field|field_type == 'passwordfield' %}
        {{ field|add_class:"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"|attr:"placeholder:" }}
      {% else %}
        <!-- Fallback -->
        {{ field }}
      {% endif %}
    </label>
  {% endfor %}
</div>

If that’s not a piece of boring server side rendered template, then I don’t know what is.

You probably also agree that HTMX is simpler than something like React for most web apps. Whether HTMX is boring or not might be a bit more controversial. After all, it’s easy to produce a mess with non-statically typed templates and HTMX. I see HTMX as an extension of good ol’ HTML that can be sprinkled on top of server side rendered templates, which is boring.

Regarding Celery , only few would agree that it’s Boring Technology. Celery is the de facto standard for tasks queues in the Python world. Yes, it can blow up in a lot of different ways, but it’s well understood in which ways. It has a lot of known unknowns, but only few unknown unknowns, which makes it boring to me.

So why do we get to keep Celery but discard React? React increases the project complexity by turning a monolithic backend into a distributed system where a part of the app runs in the user’s browser.

The alternatives to Celery would be queues like Django Q or Huey . I used both. Celery with the default configurations doesn’t seem any less simple in practice. Maybe once it blows up in my face in an unknown way it will change my mind.

Downside: It’s not always easy to define the term Boring Technology. For some, it’s technology that is not changing frequently. For others, it’s tools that they personally have used for decades. Another angle would be spending a limited amount of innovation points on a project.

Static typing

Static typing is one of the main reasons I am using Django over other frameworks like Rails. One of the design philosophies of Django is to be explicit . Being explicit seems to fundamentally lend itself better for static typing.

Static typing means a compiler or linter that understands the code by inference and looking at type annotations. schablone uses pyright instead of mypy .

Mypy was quiet slow in my tests and it had some bugs around recursive types. Mypy also requires type annotations for third party packages in order to work well, where pyright does a decent job inferring them. Pyright is developed by Microsoft and is part of the VSCode Pylance extension.

Python autocomplete

Of course, you don’t get OCaml level type safety in Python. With type annotations on methods and functions the experience is comparable to TypeScript.

Side note: It’s fascinating that pyright, a tool to type check Python code, is written in TypeScript . You would think that developers who understand Python on that level are using The pragmatic reasoning strikes a chord with me, because it strongly focuses on the customer of pyright and their needs and context.

Downside: The downside of static typing in Python is that you need to maintain type annotations and sometimes # type: ignore a few lines.

Ready for the edge

The choice of Memcached for caching might be an odd one, especially since Django 4 comes with built-in Redis support and schablone’s use of Redis as queue broker.

Caching on the same machine with the Gunicorn workers enables very fast response times globally with a multi region deployment on a platform like fly.io.

If you follow news around SQLite , I plan to do something similar for the query/read part with Django. My goal is to get sub 100ms response times across the globe.

Apart from using Memcached this edge stuff doesn’t really impact schablone.

The downsides are not clear yet, but that one is for a separate post!