Proper
A Python framework for writing web applications properly.
uvx proper_new myapp
Type this command, and a new project will appear (requires uv)
# imports
import peewee as pw
from proper.rich_text import HasRichText, RichTextField
from .base import BaseModel, scope
from .attachment import Attachment
from .user import User
class Article(BaseModel, HasRichText):
title = pw.CharField()
content = RichTextField(Attachment, null=True)
cover_image = pw.ForeignKeyField(Attachment, null=True)
is_draft = pw.BooleanField(default=True)
author = pw.ForeignKeyField(
model=User,
backref="articles",
default=lambda: current.user
)
@scope
def published(query):
return query.where(Article.is_draft==False)
@scope
def recent(query):
return (
query
.order_by(Article.created_at.desc())
.limit(25)
)
def byline(self):
return (
f"Written by {self.author.name} "
f"on {self.created_at.strftime('%b %d, %Y')}")
def publish(self):
self.is_draft = False
Peewee ORM make modelling easy.
Proper uses Peewee as its ORM, and adds some extra features like scopes, rich text fields, and attachment support.
# imports
from proper.errors import NotFound
from ..forms.article import ArticleForm
from ..models import Article
from ..router import router
from .app_controller import AppController
@router.resource("articles")
class ArticleController(AppController):
before = [
{"do":"set_article", "exclude":["index","new","create"]},
{"do":"set_form", "exclude":["index","show","delete"]},
{"do":"validate_form", "only":["create","update"]},
]
def index(self):
self.articles = Article.select().published().recent()
def show(self):
if self.article.is_draft:
raise NotFound
def new(self): pass
def edit(self): pass
def create(self):
article = self.form.save()
self.response.redirect_to("Article.show", article)
# Private
def set_article(self):
article_id = self.params.get("article_id", "")
self.article = Article.get_or_none(id=int(article_id))
if not self.article:
raise NotFound
def set_form(self):
obj = getattr(self, "article", None)
self.form = ArticleForm(self.params, object=obj)
Controllers handle all requests.
Proper controllers are simple classes with methods for each action. They have before/after hooks, and can render templates or redirect.
# imports
from proper import forms as f
from ..models import Article, Attachment
class ArticleForm(f.Form):
class Meta:
orm_cls = Article
title = f.TextField()
content = f.RichTextField(required=False)
cover_image = f.AttachmentField(Attachment, required=False)
is_draft = f.BooleanField(default=False)
Forms are declarative and easy to use.
Proper forms are declarative, and integrate with Peewee models. They also have built-in validation, and can be rendered in templates.
{#import "layouts/app.jx" as Layout #}
{#def article #}
<Layout title="Room">
<h1>{{ article.title }}</h1>
<img src="{{ article.cover_image.url }}"
alt="{{ article.title }}">
<p>{{ article.content }}</p>
{% if current.user.is_admin -%}
<a href="{{ url_for('Article.edit', article) }}">Edit</a>
{% endif %}
</Layout>
Component Views.
Proper views are components-based templates. They can have props, slots, and can be nested.
Batteries,
all of them
The boring parts of every web app - solved, documented, and ready to import. No assembly. No glue code. No "pick your favorite from the ecosystem."
Peewee ORM
A small, expressive ORM with migrations, query helpers, and async-safe connection handling.
Forms
Declarative forms with field validation, ORM integration, and rendering helpers.
Jx components
Server-rendered Python components - typed props, slots, and zero template language.
Caching
Fragment, action, and low-level caching with SQLite or Redis backends.
Background tasks
Huey-powered queue with retries, schedules, and cron syntax.
Templated transactional mail with SMTP and console mailers for development.
Authentication
Sessions, password resets, rate limiting, and pwned-password checks out of the box.
File storage
Disk and S3 adapters with signed URLs and image variants.
Rich Text Editor
Rich content, including images, to replace boring text fields.
Internationalization
Locale-aware routing, translations, pluralization, and date formats.
WebSockets
Channels, broadcasts, and presence on top of the same ASGI runtime.
Turbo
Speed up page changes and form submissions and stream partial page updates.
Scaffolds,
not snippets
Copy-paste is a tax on attention. Proper's generators produce complete, idiomatic, pre-tested files - so you read, not reassemble.
One command turns a resource into a model, controller, views, tests, and routes - wired together and ready to run.
- Almost suspicious, really.
Opinionated
by design
These are the ideas the framework will not compromise on - the rules that everything else is built around.
Humans read code. Machines just run it.
Writing code can - and should - be automated. But making that code easy to read and understand is the framework's reason for being.
Convention over configuration
You already know where things go. A Post model lives at
models/post.py. Its controller is
PostController in
controllers/post_controller.py. Its views are at
views/post/. This is not a suggestion - it is a
feature.
CRUD, always
Every controller maps URLs to the same seven actions - index, new, create, show, edit, update, delete. Remove those you don't need. Create another resource if you need more.
Sync above, async below
The runtime is ASGI. The code you write is not.
await is a hazard we've hidden behind a clean interface
- you get concurrency without the colour.
Build a Proper app, side by side with AI.
Drop the Proper skill into your skills folder and your AI agent will scaffold, edit, and refactor your app the Proper way: convention-aware, generator-first, idiomatic from the start.
Installed in your home folder, skills apply across every AI agent session.