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

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