Skip to content
code-like-djangonout logo

Django Development Skill

code-like-djangonout

Provides Django web framework expertise including project structure, models, views, admin, Celery tasks, testing, and Python best practices. Use when generating, analyzing, refactoring, or reviewing Django/Python code.

SKILL.md

Full skill instructions

Django Development Skill

When to Use

Use this skill when:

  • Writing, reviewing, or refactoring Django applications
  • Creating or modifying Django models, views, admin, forms
  • Setting up Django project structure and tooling
  • Implementing Celery tasks and signals
  • Writing Django tests
  • Configuring linters (ruff, pylint) for Python/​Django

Prerequisites Check

Before starting any Django work:

# Check Python version
python --version

# Check for .python-version file
cat .python-version 2>/​dev/​null

# Verify virtual environment is active
echo $VIRTUAL_ENV

# Check Django version
python -c "import django; print(django.VERSION)"

# Check if ruff is available
command -v ruff

Instructions

General Coding Approach

  • All naming and comments must be in English
  • Follow Python (PEP 8) and Django conventions
  • Virtual environment must be activated
  • Detect project's Python version and use appropriate features
  • Do not use type annotations (Django doesn't fully rely on them)
  • If annotations are needed, import: from __future__ import annotations

Formatting and Linting

Ruff Setup
python -m pip install ruff

Minimal .ruff.toml:

line-length = 119
indent-width = 4
target-version = "py312"
exclude = [
    "**/​migrations",
    "**/​manage.py",
]

[format]
quote-style = "single"
exclude = ["**/​manage.py"]

[lint]
select = ["ALL"]
allowed-confusables = ["ı", "'"]
mccabe.max-complexity = 15
ignore = [
    "ANN",    # annotations
    "D",      # docstrings (pydocstyle)
    "D203",   # one-blank-line-before-class
    "D213",   # multi-line-summary-second-line
    "ISC001", # implicit string concat
    "COM812", # missing trailing comma
    "ERA",    # commented out code
    "RUF012", # mutable class default
    "FBT002", # boolean default positional arg
    "TD003",  # missing todo link
    "FIX002", # line contains todo
    "PT009",  # pytest unittest assertion
    "PT019",  # pytest fixture param
    "PT027",  # pytest unittest raises
    "PGH004", # file-wide noqa
    "INP001", # implicit namespace package
]

[lint.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "double"

[lint.pylint]
max-statements = 100
max-returns = 20
max-args = 10
max-positional-args = 8
max-branches = 20

[lint.isort]
known-first-party = ["core"]
section-order = [
    "future",
    "standard-library",
    "django",
    "third-party",
    "first-party",
    "local-folder",
]

[lint.isort.sections]
django = ["django"]
Pylint Setup
python -m pip install pylint

# Generate config if missing
pylint --generate-rcfile > .pylintrc
Acceptable noqa Comments
  • # noqa: S324 - hashlib security
  • # noqa: SLF001 - Model._meta access
  • # noqa: ARG002 - unused args in Django overrides

Always ask before adding other noqa comments.


Coding Style

Quote Convention

Always use single quotes. Double quotes only for docstrings or unavoidable cases:

# ✅ Good
user_name = 'vigo'
page = request.GET.get('page')

# ✅ Acceptable
message = "vigo's number"
No Magic Values
# ❌ Bad
def check_age(user):
    if user.age > 10:
        pass

# ✅ Good
USER_MAX_AGE = 10

def check_age(user):
    if user.age > USER_MAX_AGE:
        pass
Dict Access

Always use .get() for dict access:

# ❌ Bad
value = FOO['bar']

# ✅ Good
value = FOO.get('bar')
value = FOO.get('bar', 'default')
Error Handling

Never use blind exceptions. Always handle specific exceptions:

# ❌ Bad
try:
    do_something()
except Exception:
    pass

# ✅ Good
try:
    do_something()
except SpecificError as exc:
    logger.exception('Operation failed: %s', exc)
    raise
Service-Specific Exceptions

Every service must have its own exception:

# exceptions.py
class ProjectError(Exception):
    def __init__(self, message, humans=False, **extras):
        if humans:
            message = message.title()
        super().__init__(message)
        self.humans = humans
        self.message = message
        self.extras = extras


class NotificationServiceError(ProjectError):
    ...


# services/​notification.py
import logging

logger = logging.getLogger('project.NotificationService')


class NotificationService:
    def send(self, recipient, message):
        try:
            response = ExternalAPI.send(recipient, message)
        except ExternalAPIError as exc:
            logger.exception('Failed to send notification')
            raise NotificationServiceError('Notification failed') from exc
        return response

Django Project Structure

core/
    admin/
        __init__.py
        user.py
    fixtures/
        core.user.json
    forms/
        __init__.py
        user.py
    management/
        __init__.py
        commands/
            create_foo.py
    migrations/
    models/
        __init__.py
        user.py
    services/
        __init__.py
        notification.py
    signals/
        __init__.py
        user.py
    tasks/
        __init__.py
        notification.py
    templates/
        auth/
            signin.html
    views/
        __init__.py
        auth/
            __init__.py
            login.py
    checks.py
    storage.py

AppConfig Example

# pylint: disable=W0611,C0415
# ruff: noqa: F401,PLC0415
from django.apps import AppConfig
from django.conf import settings


class CoreConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'core'

    def ready(self):
        from .signals import user_signals
        from .tasks import notification_tasks

        if settings.DEBUG:
            from .checks import check_environment_variables, check_models

Model Rules

Basic Structure
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _


class PostManager(models.Manager):
    def get_by_natural_key(self, author_email, title):
        author = get_user_model().objects.get_by_natural_key(author_email)
        return self.get(author=author, title=title)


class Post(models.Model):
    # 1. Field declarations
    created_at = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_('created at'),
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        verbose_name=_('updated at'),
    )
    title = models.CharField(
        max_length=255,
        verbose_name=_('title'),
    )
    body = models.TextField(
        blank=True,
        verbose_name=_('body'),
    )
    author = models.ForeignKey(
        to=get_user_model(),
        related_name='posts',
        related_query_name='post',
        on_delete=models.CASCADE,
        verbose_name=_('author'),
    )

    # 2. Custom managers
    objects = PostManager()

    # 3. Class Meta
    class Meta:
        app_label = 'core'
        db_table = 'post'
        verbose_name = _('Post')
        verbose_name_plural = _('Posts')

    # 4. __str__
    def __str__(self):
        return f'{self.title}'

    # 5. save (if needed)

    # 6. natural_key
    def natural_key(self):
        return (self.author.email, self.title)

    natural_key.dependencies = ['auth.user']

    # 7. get_absolute_url (if needed)
Model Method Order
  1. Field declarations
  2. Custom managers
  3. class Meta
  4. __str__
  5. save
  6. natural_key
  7. get_absolute_url
Model Checklist
RequirementExample
Manager with get_by_natural_keyobjects = PostManager()
class Meta with required attrsapp_label, db_table, verbose_name, verbose_name_plural
natural_key methodMust match manager's get_by_natural_key
verbose_name on all fieldsUse gettext_lazy: verbose_name=_('title')
Choices as callablechoices=get_language_choices
Relational fields with all kwargsto, related_name, related_query_name, on_delete
Choices

Use callable for choices (allows changes without migration):

def get_language_choices():
    return settings.LANGUAGES


class Page(models.Model):
    language = models.CharField(
        max_length=2,
        choices=get_language_choices,
        verbose_name=_('language'),
    )

Or use Django's TextChoices:

class LanguageChoices(models.TextChoices):
    ENGLISH = 'en', _('English')
    TURKISH = 'tr', _('Turkish')

Or stdlib Enum:

from enum import StrEnum


class CandidateStatus(StrEnum):
    STARTED = 'started'
    IN_PROGRESS = 'in_progress'
    COMPLETED = 'completed'
Constraints and Indexes
class Meta:
    constraints = [
        models.UniqueConstraint(
            fields=['name', 'owner'],
            condition=models.Q(owner__isnull=False),
            name='uc_org_name_owner',  # uc_<identifier>_<field>
        ),
    ]
    indexes = [
        models.Index(
            fields=['candidate_name'],
            condition=~models.Q(candidate_name=''),
            name='idx_cand_name',  # idx_<identifier>_<field>
        ),
    ]
File Upload Fields
from django.core.files.storage import FileSystemStorage


def dynamic_file_storage():
    return FileSystemStorage()


def upload_video_path(instance, filename):
    return f'videos/​{instance.pk}/​{filename}'


class Media(models.Model):
    video = models.FileField(
        upload_to=upload_video_path,
        storage=dynamic_file_storage,
        verbose_name=_('video'),
    )

Admin Rules

Admin files live in <app>/​admin/<model>.py:

from django.contrib import admin

from core.admin.base import BaseModelAdmin
from core.models import Post


@admin.register(Post)
class PostAdmin(BaseModelAdmin):
    list_display = ('title', 'author', 'created_at')
    list_display_links = ('title',)
    search_fields = ('title', 'body')
    ordering = ('-created_at',)

    # Performance for ForeignKey fields
    autocomplete_fields = ('author',)
    list_select_related = ('author',)
Minimum Admin Properties
  • list_display
  • list_display_links
  • search_fields
  • ordering

For ForeignKey fields, always add:

  • autocomplete_fields
  • list_select_related

View Rules

  • Views live in <app>/​views/
  • Only Class-Based Views (no function-based views)
  • Separate business logic into service layer
# ❌ Bad - logic in view
class OrderView(View):
    def post(self, request):
        # 50 lines of business logic here
        ...


# ✅ Good - logic in service
class OrderView(View):
    def post(self, request):
        form = self.get_form()
        if not form.is_valid():
            return self.form_invalid(form)

        service = OrderService(
            request=request,
            form=form,
            logger=self.logger,
        )
        redirect_url = service.process_order()
        return HttpResponseRedirect(redirect_url)

Internationalization

Never use hardcoded strings:

# ❌ Bad
return HttpResponse('Error')

# ✅ Good
from django.utils.translation import gettext_lazy as _

return HttpResponse(_('Error'))

In templates:

{% load i18n %}

{% translate "Welcome" %}

{% blocktranslate with name=user.name %}
Hello, {{ name }}!
{% endblocktranslate %}

Celery Tasks

Tasks live in <app>/​tasks/:

# tasks/​notification.py
from celery import shared_task

from core.services.notification import NotificationService, NotificationServiceError


@shared_task(
    bind=True,
    max_retries=3,
    default_retry_delay=60,
)
def send_notification_task(self, user_id, message):
    try:
        service = NotificationService()
        service.send(user_id, message)
    except NotificationServiceError as exc:
        self.retry(exc=exc)

Register in AppConfig.ready():

def ready(self):
    from .tasks import notification  # noqa: F401

Testing

Tests live in tests/ directory:

tests/
    test_models_post.py
    test_views_auth.py
    test_services_notification.py
    test_forms_user.py
    test_tasks_notification.py

Naming convention: test_<type>_<name>.py

Use stdlib and Django's test suites:

from django.test import TestCase


class PostModelTest(TestCase):
    def test_str_returns_title(self):
        post = Post(title='Hello')
        self.assertEqual(str(post), 'Hello')

Django System Checks

Create checks.py for custom checks:

# pylint: disable=W0613
# ruff: noqa: ARG001,SLF001
import ast
import inspect
import os

import django.apps
from django.core import checks
from django.core.exceptions import FieldDoesNotExist

DEVELOPMENT_ENVIRONMENT_VARIABLES = [
    'DJANGO_SECRET_KEY',
    'DATABASE_URL',
    # other required environment variable name
]

FIELD_VERBOSE_NAME_WHITE_LIST = ['slug']
# MODEL_NAME_WHITE_LIST = [foomodel']


@checks.register()
def check_environment_variables(app_configs, **kwargs):
    errors = []
    for var_name in DEVELOPMENT_ENVIRONMENT_VARIABLES:
        if not os.environ.get(var_name):
            errors = [
                *errors,
                checks.Error(
                    f'Missing environment variable for development: {var_name}',
                    hint=f'Set the "{var_name}" environment variable in your environment.',
                    id='core.ENV001',
                ),
            ]
    return errors


def check_model_get_argument(node, arg):
    for kw in node.value.keywords:
        if kw.arg == arg:
            return kw
    return None


def check_model_is_gettext_node(node):
    if not isinstance(node, ast.Call):
        return False

    return node.func.id == '_'


def check_model_get_field(model, node):
    if not isinstance(node, ast.Assign):
        return None
    if len(node.targets) != 1:
        return None
    if not isinstance(node.targets[0], ast.Name):
        return None
    try:
        return model._meta.get_field(node.targets[0].id)
    except FieldDoesNotExist:
        return None


def check_model_fields_verbose_name(field, node):
    verbose_name = check_model_get_argument(node, 'verbose_name')
    if field.name not in FIELD_VERBOSE_NAME_WHITE_LIST:
        if verbose_name is None:
            yield checks.Warning(
                'Field has no verbose name',
                hint='Set verbose name on the field.',
                obj=field,
                id='BLT001',
            )
        elif not check_model_is_gettext_node(verbose_name.value):
            yield checks.Warning(
                'Verbose name should use gettext _() style',
                hint='Use gettext on the verbose name.',
                obj=field,
                id='BLT002',
            )


def check_model_class_meta(class_meta, model):
    if class_meta is None:
        yield checks.Warning(
            f'Model "{model._meta.model_name}" must define class Meta',
            hint=f'Add class Meta to model "{model._meta.model_name}".',
            obj=model,
            id='BLT003',
        )
    else:
        verbose_name = None
        verbose_name_plural = None

        for node in ast.iter_child_nodes(class_meta):
            if not isinstance(node, ast.Assign):
                continue

            if not isinstance(node.targets[0], ast.Name):
                continue

            attr = node.targets[0].id

            if attr == 'verbose_name':
                verbose_name = node

            if attr == 'verbose_name_plural':
                verbose_name_plural = node

        if verbose_name is None:
            yield checks.Warning(
                'Model has no verbose name',
                hint='Add verbose_name to class Meta.',
                obj=model,
                id='BLT004',
            )

        elif not check_model_is_gettext_node(verbose_name.value):
            yield checks.Warning(
                'Verbose name in class Meta should use gettext',
                hint=f'Use gettext on the verbose_name of class Meta "{model._meta.model_name}".',
                obj=model,
                id='BLT002',
            )

        if verbose_name_plural is None:
            yield checks.Warning(
                'Model has no verbose name plural',
                hint='Add verbose_name_plural to class Meta.',
                obj=model,
                id='BLT005',
            )

        elif not check_model_is_gettext_node(verbose_name_plural.value):
            yield checks.Warning(
                'Verbose name plural in class Meta should use gettext',
                hint=f'Use gettext on the verbose_name_plural of class Meta "{model._meta.model_name}".',
                obj=model,
                id='BLT002',
            )


def check_model(model):
    """

    BLT001: Field has no verbose name.
    BLT002: Verbose name should use gettext.
    BLT003: Model must define class Meta.
    BLT004: Model has no verbose name.
    BLT005: Model has no verbose name plural.

    """
    if model._meta.model_name not in MODEL_NAME_WHITE_LIST:
        model_source = inspect.getsource(model)
        model_node = ast.parse(model_source)
        class_meta = None
        for node in model_node.body[0].body:
            if isinstance(node, ast.ClassDef) and node.name == 'Meta':
                class_meta = node

            field = check_model_get_field(model, node)
            if field is None:
                continue

            yield from check_model_fields_verbose_name(field, node)

        yield from check_model_class_meta(class_meta, model)


@checks.register(checks.Tags.models)
def check_models(app_configs, **kwargs):
    errors = []
    for app in django.apps.apps.get_app_configs():
        if app.path.find('site-packages') > -1:
            continue

        for model in app.get_models():
            for check_message in check_model(model):
                errors = [*errors, check_message]
    return errors

Pre-Commit Hooks

brew install pre-commit
pre-commit install

Minimal .pre-commit-config.yaml:

exclude: core/​migrations/
fail_fast: true
repos:
  - repo: local
    hooks:
      - id: django-check
        name: Django checks
        entry: scripts/​pre-commit/​django-check.bash
        language: script
        always_run: true
        pass_filenames: false

      - id: ruff
        name: Ruff linter
        entry: ruff check .
        language: system
        types: [python]

      - id: pylint
        name: Pylint check
        entry: pylint -rn -sn -d R0401 config core
        language: system
        types: [python]

      - id: django-test
        name: Django tests
        entry: scripts/​pre-commit/​run-tests.bash
        language: system
        types: [python]
        pass_filenames: false

scripts/​pre-commit/​django-check.bash:

#!/​usr/​bin/​env bash
set -euo pipefail

DJANGO_ENV=production python manage.py check --deploy || exit 0

scripts/​pre-commit/​run-tests.bash:

#!/​usr/​bin/​env bash
set -euo pipefail

coverage run manage.py test --failfast

Commit Messages

Format:

[claude]: <verb> <description in lowercase>

- Detail 1
- Detail 2

Fixes #123

🤖 Generated with [Claude Code](https://claude.ai/​code)
Co-Authored-By: Claude <[email protected]>

Example:

[claude]: add user notification service

- Implement NotificationService with retry logic
- Add NotificationServiceError for error handling
- Create Celery task for async notifications

Fixes #42

🤖 Generated with [Claude Code](https://claude.ai/​code)
Co-Authored-By: Claude <[email protected]>

Quick Reference

TaskCommand
Check Python versionpython --version
Run linterruff check .
Format coderuff format .
Run pylintpylint config core
Run testspython manage.py test
Run with coveragecoverage run manage.py test
Django checkpython manage.py check
Django check deploypython manage.py check --deploy
Make migrationspython manage.py makemigrations
Apply migrationspython manage.py migrate

Resources

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View