When you need a form to create and edit a record, you don’t have to rewrite the familiar GET/POST view pair. Django already ships two class-based views built for exactly this job — CreateView and UpdateView: just declare the model, the list of fields and the template, and Django handles the rest — rendering the form, validating it, saving and redirecting. These two views fit best when your form maps directly onto a model through a ModelForm.
The Problem
In a blog app, a function-based view for creating a post is usually about 25 lines long: check request.method, build the form from request.POST or an empty form, call is_valid(), save and redirect. The view for editing a post is nearly identical, differing only in the line that fetches the object with get_object_or_404 and passes instance=post. The two views are 80% the same, yet you still have to write and maintain them separately — and the more models you have, the more duplicated code piles up.
# views.py — function-based approach: long and repetitive from django.shortcuts import get_object_or_404, redirect, render from .forms import PostForm from .models import Post def post_create(request): if request.method == "POST": form = PostForm(request.POST) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() return redirect("post-detail", pk=post.pk) else: form = PostForm() return render(request, "blog/post_form.html", {"form": form}) def post_update(request, pk): post = get_object_or_404(Post, pk=pk) if request.method == "POST": form = PostForm(request.POST, instance=post) if form.is_valid(): form.save() return redirect("post-detail", pk=post.pk) else: form = PostForm(instance=post) return render(request, "blog/post_form.html", {"form": form}) The Solution
The two class-based views below do exactly the same job, and each one is only 4–5 lines of declarations. CreateView builds a ModelForm automatically from model and fields, renders the template following the /_form.html convention, saves the object when the form is valid and redirects to success_url. UpdateView works the same way, except that it fetches the object itself from the pk in the URL and pre-fills the form with that instance. Because success_url is evaluated when the module is imported — before the URLconf has finished loading — you must use reverse_lazy() instead of reverse().
# views.py — same feature, declarations only from django.urls import reverse_lazy from django.views.generic import CreateView, UpdateView from .models import Post class PostCreateView(CreateView): model = Post fields = ["title", "content", "category"] template_name = "blog/post_form.html" success_url = reverse_lazy("post-list") class PostUpdateView(UpdateView): model = Post fields = ["title", "content", "category"] template_name = "blog/post_form.html" success_url = reverse_lazy("post-list") # urls.py from django.urls import path from .views import PostCreateView, PostUpdateView urlpatterns = [ path("posts/new/", PostCreateView.as_view(), name="post-create"), path("posts/<int:pk>/edit/", PostUpdateView.as_view(), name="post-update"), ] Two common situations: you need to set the author before saving — override form_valid(), set form.instance.author, then call super().form_valid(form). You need to stop other people editing posts that are not theirs — override get_queryset() so UpdateView only looks up posts belonging to the logged-in user; anyone else’s post returns a 404 instead of opening the form.
# views.py — finished version: assign author + block editing other people's posts from django.urls import reverse_lazy from django.views.generic import CreateView, UpdateView from .models import Post class PostCreateView(CreateView): model = Post fields = ["title", "content", "category"] template_name = "blog/post_form.html" def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) def get_success_url(self): return reverse_lazy("post-detail", kwargs={"pk": self.object.pk}) class PostUpdateView(UpdateView): model = Post fields = ["title", "content", "category"] template_name = "blog/post_form.html" success_url = reverse_lazy("post-list") def get_queryset(self): # Only look up your own posts: other people's posts return 404 return Post.objects.filter(author=self.request.user) Apply It to Your Project
Audit the create/edit views in your project: any GET/POST function pair repeated for each model can be replaced with CreateView/UpdateView following the pattern above. Start with the simplest model — a category, for example — run one create and one edit round of tests, then move on to models with their own logic, such as assigning an author or checking ownership.
Related Posts
Django: Write Forms Twice as Fast with ModelForm — Stop Mapping Every Field by Hand
Django: Replace Repetitive Function Views with ListView and DetailView
Django: Stop Hardcoding URLs in Views and Templates — Use reverse() and reverse_lazy() in 5 Minutes
Frequently Asked Questions
Can CreateView and UpdateView share one template?
Yes — both render the template following the /_form.html convention, so a single post_form.html template serves both creating and editing. To tell them apart, check form.instance.pk in the template: it is None for a new post and has a value when editing.
When should I use fields, and when form_class?
Use fields when the form is simple and you just need to quickly list the columns that may be edited. Use form_class with a dedicated ModelForm when you need custom widgets, clean_ methods or English labels — in that case, do not declare fields anymore.
Why must success_url use reverse_lazy() instead of reverse()?
Because success_url is a class attribute, evaluated as soon as the module is imported — at that point the URLconf has not finished loading, so reverse() raises an error. reverse_lazy() defers the URL lookup until the redirect is actually needed. If you want a redirect that depends on the object just created, override get_success_url() as in the example above.
Does UpdateView stop other people editing my posts on its own?
No. By default UpdateView fetches the object by pk without checking ownership, so any logged-in user can edit any record. To block that, override get_queryset() to filter by request.user as in the example above, or combine it with LoginRequiredMixin and UserPassesTestMixin.
💬 Have you tried this in your own project? If you hit an error or have a neater way to do it, leave a comment below — I read them all and will fold the best ideas into the post.
References
Django documentation — Generic editing views: https://docs.djangoproject.com/en/6.1/ref/class-based-views/generic-editing/
Django documentation — ModelForm: https://docs.djangoproject.com/en/6.1/topics/forms/modelforms/
