Plain is headed towards 1.0! Subscribe for development updates →

Principles

Making progress means making decisions. Here are some core concepts to help.

 

  1. Be painfully obvious
  2. Choose a side
  3. Get out of the way
  4. Read the source
  5. Focus on the present

Be painfully obvious

Once upon a time, the act of writing code was hard — so much typing! You were naturally incentivized to write less. It encouraged being terse and clever, resulting in code that is hard to follow.

But this is not the era of preserving keystrokes. Between good IDEs, autocomplete, and AI, the balance is tipping back. We don't need to be clever, now we can be obvious. Blatant clarity is good for both humans and machines.

Plain encourages obvious code by design. In many cases, it is intentionally verbose and there will be more code on your side of the fence than you're used to. We want you to see what is happening, not hide it away. The framework is part of your app — you didn't have to write but you do need to understand it.

@register

There are many places where you need to tie your code into the framework. A common pattern you'll see is the @register_* decorator. Not all frameworks work this way, but we want you to literally see where that connection is happening, and give you the opportunity to click through and source dive.

@register_model
class MyModel(Model):
    pass

vs

class MyModel(Model):
    pass

Required kwargs

If a function takes more than one positional argument then we try to require keyword-only arguments by using the def f(*, a, b, c) syntax. This adds clarity on both sides and makes it easier to rename or reorder params later.

def send_email(*, from_email, to_email, subject, body):
    pass

send_email(from_email="[email protected]", to_email="[email protected]", subject="Hey!", body="...")

vs

def send_email(from_email, to_email, subject, body):
    pass

send_email("[email protected]", "[email protected]", "Hey!", "...")

Greppability

We try to use unique names that fit the context. Sometimes they get long. The more unique and domain-driven a name is, the better.

If you searched across the codebase, could you find what you're looking for and nothing else?

def process_order_data(order_data):
    pass

vs

def process(data):
    pass

Choose a side

A framework is more than just a collection of libraries — it is a cohesive set of decisions and opinions about how to build. Given the opportunity, Plain chooses to support the most common use case. There is some bias baked in there, but that's the point! By default, we favor what we consider to be the typical, "happy path" that aligns with our vision. Sometimes we have to pick a direction just so we don't leave users needlessly wandering in the dark.

When it comes to code, there is often more than one way to do something. And for a framework these decisions become fundamental to its aesthetic. Like The Zen of Python, Plain prefers choosing one way to do something. Sometimes we'll choose two ways for better usability, but never three.

Database-backed cache

The plain.cache package does not ship with a set of "backends". There is only one backend and that is the database! This can take you quite far, maybe even all the way.

If you find yourself needing a more comprehensive caching solution, you're generally better off pulling in a library made specifically for that cache and taking advantage of all the advanced features it has to offer.

HTTPS in development

In plain.dev we made the decision to use HTTPS in local development. In some ways this adds extra complication to your local environment (certificates require extra tooling), but now we can assume HTTPS everywhere! Unsecured HTTP now becomes the exception, and the entire framework can ship a default experience built for HTTPS that you don't have to fight against.

Class-based views

Plain uses classes for views, as opposed to functions. On the surface, it's true that functions can be simpler, and that supporting both is feasible. But if we don't make a choice, then we leave every user with this question on every new view.

If we go all-in on one path, interesting features and benefits start to reveal themselves, and often with simpler code.


Get out of the way

Generating code is getting easier by the day, and frameworks themselves no longer need to support every use case under the sun. There comes a time when we simply need to provide the foundation and get out of your way! No amount of knobs and switches can beat "take it and run with it" — especially when those knobs and switches come up short, and you have no way to quickly and directly impact them.

Striking this balance between "dependency code" and "your code" is an important aspect of Plain.

OAuth providers

The plain.oauth package includes the fundamentals for integrating OAuth providers, but contrary to what you might expect, it doesn't ship any specific OAuth providers themselves. We want you to own that part of the code!

We can provide some starting points to be copied & pasted, but true integration with your app quickly becomes a custom problem (how to treat email addresses, usernames, avatars, etc.). And if something critical breaks with the provider, you will probably notice and be able to implement a solution before an upstream patch would even be considered.

Manual form rendering

Unlike other frameworks, Plain forms don't know how to render themselves. The frontend is a moving target and one obvious place that you can differentiate your product. Your final HTML (or JavaScript) should be just that — yours!

If you find yourself wanting a more repeatable system, don't be afraid to create one. Between Jinja features, component libraries, and just some good ol' Python, you can probably design a better, simpler system for your situation than we ever could.

Plain Python

Ultimately, you should be able to unroll most parts of Plain and replace them with your own, if you so choose. You can still get the benefit of the core underlying web framework.

Don't like our views? Write your own. Hate forms? Skip them. Prefer a JS frontend? Go for it!

The multi-package nature of Plain, and hopefully some underlying design decisions, should make for a hackable platform that can grow in the direction you need.


Read the source

You are strongly encouraged to read the source code, which encourages us to write code meant to be read!

Practically speaking, we do a few things to encourage reading the source. For one, the docs themselves are embedded in the code — markdown files right next to Python.

README.md

Every significant directory will have a README.md. If you look in your virtual environment or run plain docs, you will see the markdown.

The markdown files provide high-level overviews, but won't include extensive documentation about the lesser-used features. For more depth, you should just look at the code itself.

@internalcode

We use several tactics to try to make it clear which code you should not rely on. Some modules are literally named internal, and many variables and functions use leading _ prefixes.

There are also times when it's either not possible or just too annoying to prefix with _, and for those we use the @internalcode decorator.

Typed APIs

Modern Python typing is a useful tool, and there's more than one way to use it. Many concepts in Plain don't work with strict type-checking, but type annotations themselves can provide immense value to readers and IDEs.

Where possible, type annotations should be used to communicate intent. In some cases, type annotations can aid in runtme behavior. In the future, we may be able to move towards true type-checking.


Focus on the present

We don't try to predict the future, and we don't dwell on the past.

We try to focus on the problem of today, and keep the solution as simple as necessary. If it feels overcomplicated then it probably is — simplify! A Plain solution gets right to the heart of the issue. It doesn't have to be overly elaborate (especially at the beginning).

Listen to users

User feedback can't be disregarded. Maybe they didn't use the right words or propose the right solution, but there is always something we can do that would be an improvement.

Change your mind

Technology moves quickly. We don't necessarily have to keep pace (and sometimes "wait and see" is the right strategy), but we do need to constantly re-evaluate. Context changes. Past decisions can become irrelevant overnight.

Break stuff

If we're afraid of change, then we will get stuck. The only thing worse than a bad API is a bad API that won't be fixed!

Clearly we don't want to break things every day, but we do need to strike a balance where change is encouraged. Tooling, timing, and expectations can all help.