Changes
The gist of what's been removed or changed from Django.
Multiple packages
Plain is split into multiple, official packages.
With plain
itself,
you can write views to handle requests and responses,
render templates,
and process forms.
Other optional features like plain.models
are available as independent packages.
Database optional
As mentioned above, plain.models
and database integration is optional.
This means you can start projects without a database
(more like an alternative to Flask)
and add the database later.
Removal of "old" code
A lot of django.contrib
has been removed (gis
, sites
, sitemaps
, etc.).
Most of those concepts could be added back via independent packages.
Leveraging Python packages
Plain replaces some Django features with extremely common Python packages:
- Jinja for templates (exclusively — no Django Template Language)
- Click for CLIs (available as
plain
) - Pytest for testing (based on
pytest-django
) - Gunicorn (instead of development "runserver")
Jinja for templates
Jinja is the exclusive template language in Plain.
In part this is because Jinja is so prevalent in the Python ecosystem.
But it's also a personal preference of mine at this point —
I've found in recent years that I was spending way too time and energy working around the simplicity of the Django template language.
The lack of function(param1, param2)
syntax in Django templates meant that I'd create all kinds of tags, filters, and 0-parameter methods as solutions that started to feel like workarounds.
Rewritten views
Views are entirely class-based, and rewritten with the hopes of being simpler.
Views can also return simple types (str
-> text, dict
-> JSON, etc.) which are converted to HTTP responses.
from plain.views import View
class ExampleView(View):
def get(self):
return "Hello, world!" # A plain text response
.env
integration
Plain can automatically load environment variables from a .env
file from the top of your repo.
To specify a different .env
file, use PLAIN_ENV
. For example, PLAIN_ENV=dev
will load .env.dev
.
Removed form rendering
Plain is not responsible for form rendering — you are! This can mean a little more work on the HTML side, but also emphasizes more control over the frontend appearance and behavior of forms.
<form method="post">
{{ csrf_input }}
<!-- Render general form errors -->
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
<!-- Render form fields individually (or with Jinja helps or other concepts) -->
<label for="{{ form.email.html_id }}">Email</label>
<input
type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.html_id }}"
value="{{ form.email.value() or '' }}"
autocomplete="email"
autofocus
required>
{% if form.email.errors %}
<div>{{ form.email.errors|join(', ') }}</div>
{% endif %}
<button type="submit">Save</button>
</form>
To re-use form rendering across your app,
you can make use of Jinja feautres like macros and includes,
plain-elements
, or anything else you can come up with!
Apps -> Packages
"Apps" have been renamed to "packages". Plain considers your entire project to be an "app".
Settings changes
Any package in INSTALLED_PACKAGES
can define their own default_settings.py
,
which will be loaded automatically.
Settings can also use type annotations, which will be checked at runtime.
# github/default_settings.py
GITHUB_API_BASE_URL: str = "https://api.github.com"
GITHUB_TOKEN: str # A setting is required if it has a type annotation, but no default value
Project file structure
A Plain project has a more opinionated file structure.
- The
app
directory for your code. - Settings at
app/settings.py
. - No
manage.py
,wsgi.py
, orasgi.py
.
Combined with a lot of other changes in Plain, a simple web server can look more like this:
.
├── app
│ ├── settings.py
│ ├── templates
│ │ ├── 404.html
│ │ ├── base.html
│ │ └── example.html
│ └── urls.py
├── uv.lock
└── pyproject.toml
2 directories, 7 files
Async removed
Async and ASGI have been removed, for now.
i18n removed
Internationalization has been removed, for now.
No base auth.User
The plain-auth
package doesn't come with a base User
model anymore.
This gives you complete control over the fields on your User model,
and also requires you to create one.
Here's an example from the plain-starter-app:
from plain.models import models
from plain.passwords.models import PasswordField
class User(models.Model):
email = models.EmailField(unique=True)
password = PasswordField()
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.email
No media, STORAGES
, or FileField
I expect some version of these things to come back, but for now they've been removed.
In practice I've found myself skipping these in favor of uploading to S3 (or similar) directly, sometimes in the frontend via a signed-url, and then storing the URL in the database and managing the files myself...
Other notable changes
- Requests have a
unique_id
. - Removed
SCRIPT_NAME
andscript_prefix
. - Removed fixtures.
- Static files are served automatically (Whitenoise is integrated).