Guide: Documenting a Django Legacy Codebase
Django projects are notorious for accumulating undocumented code over time. Views that do too much, model methods without descriptions, and templates with no context become a maintenance nightmare. Documenting legacy Django apps requires a different approach than starting fresh.
Why Django Documentation Matters
Unlike vanilla Python, Django has specific documentation needs:
- Views: HTTP handlers with request/response contracts
- Models: Database schema with business logic
- Serializers: Data transformation for APIs
- URLs: Route-to-view mappings
The Django Documentation Challenge
Legacy Django codebases face unique hurdles:
- Massive views: 200+ line view functions mixing business logic and HTTP handling
- Implicit contracts: Request data shapes are never documented
- Template context: Context variables are not type-safe or documented
- Model relationships: Foreign key chains are hard to trace
A Systematic Documentation Approach
Phase 1: Audit Models First
Start with your models—they define the data structures everything else depends on. Identify:
- Models without docstrings on fields or methods
- Custom methods that don't follow Django conventions
- Verbose_name and help_text that are missing or inconsistent
Phase 2: Document Views
Views need to document their request/response contracts. Focus on:
- Expected request parameters (GET, POST, JSON body)
- Response formats (HTML template, JSON, redirect)
- Authentication/permission requirements
Phase 3: API Layer
For Django REST Framework projects, document serializers and viewsets:
- Input/output schemas
- Field-level validation rules
- Permission classes
Example: Documenting a Django View
Before (undocumented):
def invoice_report(request):
if not request.user.is_authenticated:
return redirect('login')
month = request.GET.get('month')
year = request.GET.get('year')
invoices = Invoice.objects.filter(
month=month, year=year
).select_related('customer')
total = invoices.aggregate(Sum('amount'))['amount__sum']
return render(request, 'reports/invoice.html', {
'invoices': invoices,
'total': total,
'month': month,
'year': year
})
After (documented):
def invoice_report(request):
"""
Generate an invoice report for a specific month and year.
Requires authentication. Uses select_related to minimize DB queries
when fetching invoice data with customer information.
Args:
request: HttpRequest object with user authentication and GET params
Query Parameters:
month: Month to filter invoices (1-12)
year: Year to filter invoices (e.g., 2024)
Returns:
HttpResponse: Rendered HTML template with invoice list, total,
and the month/year parameters passed back for context
Permissions:
login_required: Only authenticated users can access
Template:
reports/invoice.html
"""
if not request.user.is_authenticated:
return redirect('login')
month = request.GET.get('month')
year = request.GET.get('year')
invoices = Invoice.objects.filter(
month=month, year=year
).select_related('customer')
total = invoices.aggregate(Sum('amount'))['amount__sum']
return render(request, 'reports/invoice.html', {
'invoices': invoices,
'total': total,
'month': month,
'year': year
})
Example: Documenting a Django Model
Before:
class Invoice(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
month = models.IntegerField()
year = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
def total_with_tax(self):
return self.amount * 1.1
After:
class Invoice(models.Model):
"""
Represents a single customer invoice in the billing system.
Each invoice is tied to a Customer and tracks amount, period,
and system timestamps. The amount field is in the company's
base currency.
"""
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
help_text='Customer this invoice belongs to'
)
amount = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name='Invoice Amount',
help_text='Total amount in base currency (no tax)'
)
month = models.IntegerField(verbose_name='Billing Month')
year = models.IntegerField(verbose_name='Billing Year')
created_at = models.DateTimeField(auto_now_add=True)
def total_with_tax(self):
"""
Calculate the invoice total including standard VAT rate.
Returns:
Decimal: Amount with 10% tax added
"""
return self.amount * Decimal('1.1')
Tools That Help
The Legacy Doc-AI CLI can parse Django projects and generate docstrings for views, models, serializers, and more. It respects Django conventions and can integrate with your existing code style. Run batch scans to find undocumented code, review diffs, and apply changes systematically.
Conclusion
Documentation is especially valuable for Django legacy codebases. It helps onboard new developers, reduces the risk of breaking implicit contracts, and makes refactoring safer. A systematic approach—models first, then views, then APIs—combined with diff review tools makes the process manageable.