Skip to main content

Python Coding Standards

This guide establishes the conventions and standards that the backend development team must follow to maintain consistency and quality in Python code, following PEP 8 and community best practices.

Naming Conventions

Variables and Functions

Use snake_case for variables and functions:

# ✅ Correct
user_name = 'john_doe'
user_age = 25

def calculate_total_price(items):
"""Calculates the total price of a list of items."""
return sum(item.price for item in items)

# ❌ Incorrect
userName = 'john_doe'
UserAge = 25

def calculateTotalPrice(items):
return sum(item.price for item in items)

Use descriptive and meaningful names:

# ✅ Correct
active_user_count = 150

async def fetch_user_profile(user_id: int) -> User:
"""Gets a user's profile by their ID."""
pass

# ❌ Incorrect
count = 150

async def fetch(id: int):
pass

Classes

Use PascalCase for class names:

# ✅ Correct
class UserService:
def __init__(self):
pass

class EmailNotificationHandler:
def send_email(self):
pass

# ❌ Incorrect
class userService:
def __init__(self):
pass

class email_notification_handler:
def send_email(self):
pass

Constants

Use SCREAMING_SNAKE_CASE for constants:

# ✅ Correct
MAX_RETRY_ATTEMPTS = 3
API_BASE_URL = 'https://api.example.com'
DEFAULT_TIMEOUT = 5000

# ❌ Incorrect
maxRetryAttempts = 3
apiBaseUrl = 'https://api.example.com'

Modules and Packages

Use snake_case for module and package names:

# ✅ Correct
import user_service
from email_handlers import notification_sender

# ❌ Incorrect
import UserService
from EmailHandlers import NotificationSender

Private Variables

Use underscore prefix to indicate privacy:

# ✅ Correct
class UserService:
def __init__(self):
self._private_var = "private"
self.__very_private = "very private"

def _private_method(self):
"""Private method for internal use."""
pass

def public_method(self):
"""Public API method."""
pass

# ❌ Incorrect
class UserService:
def __init__(self):
self.privateVar = "private"

def privateMethod(self):
pass

File and Directory Organization

Directory Structure

Django Backend Project Structure with Laravel-like Organization:

This structure follows Django's modular, application-based architecture where each feature is organized into separate Django apps, promoting separation of concerns and reusability.

project_name/
├── manage.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── entrypoint.sh
├── project_name/ # Main Django project configuration
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── core/ # Core shared utilities and models
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── auth/ # Authentication and authorization
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── api/ # API endpoints and routing
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ └── controllers/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── feature_app_name/ # Feature-specific apps (products, orders, notifications, etc.)
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
└── static/ # Static files

Key Principles:

  • Modular Architecture: Each major feature is a separate Django app
  • Laravel-like Structure: http/controllers/ and http/services/ for better organization
  • Separation of Concerns: Clear distinction between models, controllers, services, and serializers
  • Django + DRF: Built exclusively with Django and Django REST Framework
  • Scalable: Easy to add new feature apps following the same pattern

File Naming Conventions

Use PascalCase for Controller, Model, Service, and Serializer files:

# ✅ Correct - Controllers
SomeController.py

# ✅ Correct - Models
SomeModel.py

# ✅ Correct - Services
SomeService.py

# ✅ Correct - Serializers
SomeSerializer.py

# ✅ Correct - Test Cases
SomeTestCase.py

Standard Django App Structure:

app_name/
├── config/ # App configuration
├── http/
│ ├── controllers/ # PascalCase controller files
│ └── services/ # PascalCase service files
├── models/ # PascalCase model files
├── serializers/ # PascalCase serializer files
├── migrations/ # Django migrations
├── tests/ # PascalCase test files
├── admin.py # Django admin configuration
├── apps.py # Django app configuration
├── urls.py # URL patterns
└── views.py # Django views

Internal File Organization

Import order (PEP 8):

# 1. Standard library imports
import os
import sys
from datetime import datetime
from typing import List, Optional

# 2. Third-party imports
import fastapi
import sqlalchemy
from pydantic import BaseModel

# 3. Local/internal imports
from core.services.user_service import UserService
from infrastructure.repositories.user_repository import UserRepository
from shared.exceptions.validation_error import ValidationError

Class element order:

class UserService:
"""Service for user management."""

# 1. Class attributes
MAX_LOGIN_ATTEMPTS = 3

def __init__(self, user_repository: UserRepository):
"""Initialize the user service."""
# 2. Instance attributes
self._user_repository = user_repository
self._logger = logging.getLogger(__name__)

# 3. Public methods
async def create_user(self, user_data: CreateUserDto) -> User:
"""Create a new user in the system."""
pass

def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Get a user by their ID."""
pass

# 4. Private methods
def _validate_user_data(self, user_data: CreateUserDto) -> bool:
"""Validate user data."""
pass

def _hash_password(self, password: str) -> str:
"""Generate password hash."""
pass

Package Structure

Use __init__.py to define public API:

# src/core/services/__init__.py
"""Domain services."""

from .user_service import UserService
from .email_service import EmailService

__all__ = ['UserService', 'EmailService']

Documentation and Comments

Docstrings

Use docstrings for functions, methods, and classes (PEP 257):

def create_user(user_data: CreateUserDto, options: Optional[CreateUserOptions] = None) -> User:
"""
Create a new user in the system.

Args:
user_data: User data to create
options: Additional options for creation

Returns:
Created user with assigned ID

Raises:
ValidationError: When user data is invalid
DuplicateEmailError: When email already exists

Example:
>>> user_data = CreateUserDto(email="test@example.com", name="Test")
>>> user = create_user(user_data)
>>> print(user.id)
1
"""
# Implementation...

Use docstrings for classes:

class UserService:
"""
Service for system user management.

This service handles all user-related operations,
including creation, authentication, and profile management.

Attributes:
_user_repository: Repository for user data access
_logger: Logger for event recording

Example:
>>> user_service = UserService(user_repository)
>>> user = await user_service.create_user(user_data)
"""

def __init__(self, user_repository: UserRepository):
"""
Initialize the user service.

Args:
user_repository: Repository for data access
"""
self._user_repository = user_repository

Use inline comments for complex logic:

# Apply password hash using bcrypt with configurable salt rounds
salt_rounds = config.get('BCRYPT_SALT_ROUNDS', 10)
hashed_password = bcrypt.hashpw(user_data.password.encode('utf-8'),
bcrypt.gensalt(rounds=salt_rounds))

# Verify that email is not already registered before creating user
existing_user = await self._user_repository.find_by_email(user_data.email)
if existing_user:
raise DuplicateEmailError('Email already registered')

Avoid obvious comments:

# ❌ Incorrect - obvious comment
user_id = user.id # Get the user ID

# ✅ Correct - no unnecessary comment
user_id = user.id

# ✅ Correct - useful comment
user_id = user.id # Use ID for change auditing

API Documentation

Use docstrings for FastAPI endpoints:

@app.post("/api/users", response_model=User)
async def create_user(user_data: CreateUserDto) -> User:
"""
Create a new user.

Creates a new user in the system with the provided data.

Args:
user_data: User data to create

Returns:
Created user with assigned ID

Raises:
HTTPException: 400 if data is invalid
HTTPException: 409 if email already exists
"""
return await user_service.create_user(user_data)

README and Module Documentation

Each main module should have a README.md:

# User Service Module

## Description
Module responsible for system user management.

## Components
- `UserController`: Handles HTTP requests related to users
- `UserService`: Contains business logic for users
- `UserRepository`: User data access

## Usage
```python
from core.services.user_service import UserService
from infrastructure.repositories.user_repository import UserRepository

user_repository = UserRepository()
user_service = UserService(user_repository)
new_user = await user_service.create_user(user_data)

Dependencies

  • bcrypt: For password hashing
  • PyJWT: For JWT token generation
  • SQLAlchemy: For database ORM

### TODO and FIXME Comments

**Use standard format for pending tasks:**

```python
# TODO: Implement more robust email validation
# FIXME: Handle edge case when user doesn't have permissions
# HACK: Temporary solution until new auth system is implemented
# NOTE: This method will be deprecated in version 2.0

Code Best Practices

Functions and Methods

Keep functions small and with single responsibility:

# ✅ Correct - function with single responsibility
def validate_email(email: str) -> bool:
"""Validate email format using regex."""
import re
email_regex = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'$'
return re.match(email_regex, email) is not None

async def hash_password(password: str) -> str:
"""Generate password hash using bcrypt."""
import bcrypt
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')

# ❌ Incorrect - function with multiple responsibilities
async def process_user(user_data: dict) -> dict:
"""Process user data (does too many things)."""
# Validate email
if '@' not in user_data['email']:
raise ValueError('Invalid email')

# Hash password
user_data['password'] = await hash_password(user_data['password'])

# Save to database
return await database.save(user_data)

Error Handling

Use specific exception classes:

# ✅ Correct
class ValidationError(Exception):
"""Data validation error."""

def __init__(self, message: str, field: str):
super().__init__(message)
self.field = field

class NotFoundError(Exception):
"""Error when a resource is not found."""

def __init__(self, resource: str, identifier: str):
message = f"{resource} with id {identifier} not found"
super().__init__(message)
self.resource = resource
self.identifier = identifier

# Usage
if not user:
raise NotFoundError('User', str(user_id))

Using Type Hints

Define explicit types using typing:

from typing import List, Optional, Dict, Any
from pydantic import BaseModel

# ✅ Correct
class CreateUserDto(BaseModel):
"""DTO for user creation."""
email: str
password: str
first_name: str
last_name: str
role: Optional[str] = None

async def create_user(user_data: CreateUserDto) -> User:
"""Create a new user."""
pass

def get_users_by_role(role: str) -> List[User]:
"""Get users by role."""
pass

# ❌ Incorrect
async def create_user(user_data): # No type hints
pass

Use Enums for constant values:

from enum import Enum

# ✅ Correct
class UserRole(str, Enum):
"""Available user roles."""
ADMIN = 'admin'
USER = 'user'
MODERATOR = 'moderator'

class HttpStatus(int, Enum):
"""HTTP status codes."""
OK = 200
CREATED = 201
BAD_REQUEST = 400
UNAUTHORIZED = 401
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500

# Usage
user = User(role=UserRole.ADMIN)

Using Dataclasses and Pydantic

Use dataclasses for simple data structures:

from dataclasses import dataclass
from datetime import datetime

@dataclass
class User:
"""User entity."""
id: int
email: str
first_name: str
last_name: str
created_at: datetime
is_active: bool = True

Use Pydantic for validation and serialization:

from pydantic import BaseModel, EmailStr, validator

class CreateUserRequest(BaseModel):
"""Request to create user."""
email: EmailStr
password: str
first_name: str
last_name: str

@validator('password')
def validate_password(cls, v):
"""Validate that password has at least 8 characters."""
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
return v

Linting and Formatting

Flake8 Configuration

Install linting tools:

pip install flake8 flake8-docstrings flake8-import-order flake8-bugbear
pip install mypy # For type checking

.flake8 configuration:

[flake8]
max-line-length = 88
extend-ignore =
E203, # whitespace before ':'
E501, # line too long (handled by black)
W503, # line break before binary operator
D100, # Missing docstring in public module
D104, # Missing docstring in public package
exclude =
.git,
__pycache__,
.venv,
venv,
migrations,
.pytest_cache,
build,
dist
per-file-ignores =
__init__.py:F401,D104
tests/*:D100,D101,D102,D103,D104
*/settings/*:D100,D101,D102,D103
*/migrations/*:D100,D101,D102,D103,D104
docstring-convention = google
import-order-style = google
application-import-names = core,auth,api

Black Configuration

Install Black:

pip install black

pyproject.toml configuration:

[tool.black]
line-length = 88
target-version = ['py39', 'py310', 'py311']
include = '\.pyi?$'$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| migrations
)/
'''

Pre-commit Configuration

Install pre-commit:

pip install pre-commit

.pre-commit-config.yaml configuration:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict

- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3

- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [django-stubs, djangorestframework-stubs]

Install hooks:

pre-commit install

Django-Specific Standards

Django Project Structure

Application organization:

project_name/
├── manage.py
├── requirements.txt
├── core/ # Main configuration and shared utilities
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # Base configuration
│ │ ├── development.py # Development configuration
│ │ ├── production.py # Production configuration
│ │ └── testing.py # Test configuration
│ ├── urls.py
│ ├── wsgi.py
│ └── utils/ # Shared utilities
├── auth/ # Authentication and authorization
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ └── tests/
├── api/ # API endpoints
│ ├── v1/
│ │ ├── urls.py
│ │ └── views.py
│ └── middleware/
└── apps/ # Feature-specific applications
├── products/
├── orders/
└── notifications/

Django Models

Model conventions:

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

class User(AbstractUser):
"""
Custom user model.

Extends Django's default user model to add
business-specific additional fields.
"""
email = models.EmailField(_('email address'), unique=True)
first_name = models.CharField(_('first name'), max_length=150)
last_name = models.CharField(_('last name'), max_length=150)
is_verified = models.BooleanField(_('is verified'), default=False)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']

class Meta:
verbose_name = _('User')
verbose_name_plural = _('Users')
db_table = 'auth_user'

def __str__(self) -> str:
"""String representation of the user."""
return f"{self.first_name} {self.last_name} ({self.email})"

def get_full_name(self) -> str:
"""Return the user's full name."""
return f"{self.first_name} {self.last_name}".strip()

Use Abstract Base Models for common functionality:

class TimestampedModel(models.Model):
"""
Abstract model that provides timestamp fields.

All models that inherit from this class will automatically have
created_at and updated_at fields.
"""
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)

class Meta:
abstract = True

class Product(TimestampedModel):
"""Product model."""
name = models.CharField(_('name'), max_length=200)
description = models.TextField(_('description'), blank=True)
price = models.DecimalField(_('price'), max_digits=10, decimal_places=2)
is_active = models.BooleanField(_('is active'), default=True)

class Meta:
verbose_name = _('Product')
verbose_name_plural = _('Products')
ordering = ['-created_at']

def __str__(self) -> str:
return self.name

Django REST Framework Serializers

Serializer conventions:

from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Product

User = get_user_model()

class UserSerializer(serializers.ModelSerializer):
"""
Serializer for the User model.

Provides complete user serialization including
read-only fields like created_at and updated_at.
"""
full_name = serializers.CharField(source='get_full_name', read_only=True)

class Meta:
model = User
fields = [
'id', 'email', 'first_name', 'last_name', 'full_name',
'is_verified', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'is_verified']

class CreateUserSerializer(serializers.ModelSerializer):
"""
Serializer for creating new users.

Includes password validation and password confirmation.
"""
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)

class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'password', 'password_confirm']

def validate(self, attrs):
"""Validate that passwords match."""
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError("Passwords don't match")
return attrs

def create(self, validated_data):
"""Create a new user with hashed password."""
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user

class ProductSerializer(serializers.ModelSerializer):
"""Serializer for the Product model."""

class Meta:
model = Product
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']

def validate_price(self, value):
"""Validate that price is positive."""
if value <= 0:
raise serializers.ValidationError("Price must be positive")
return value

Views and ViewSets

Use DRF ViewSets for RESTful APIs:

from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from .models import Product
from .serializers import UserSerializer, CreateUserSerializer, ProductSerializer

User = get_user_model()

class UserViewSet(viewsets.ModelViewSet):
"""
ViewSet for user management.

Provides complete CRUD operations for users with
appropriate permissions and action-specific serializers.
"""
queryset = User.objects.all()
permission_classes = [permissions.IsAuthenticated]

def get_serializer_class(self):
"""Return the appropriate serializer based on action."""
if self.action == 'create':
return CreateUserSerializer
return UserSerializer

def get_permissions(self):
"""Return action-specific permissions."""
if self.action == 'create':
# Allow public registration
return [permissions.AllowAny()]
return super().get_permissions()

@action(detail=False, methods=['get'])
def me(self, request):
"""Endpoint to get current user information."""
serializer = self.get_serializer(request.user)
return Response(serializer.data)

@action(detail=True, methods=['post'])
def verify_email(self, request, pk=None):
"""Endpoint to verify user email."""
user = self.get_object()
# Verification logic here
user.is_verified = True
user.save()
return Response({'status': 'email verified'})

class ProductViewSet(viewsets.ModelViewSet):
"""ViewSet for product management."""
queryset = Product.objects.filter(is_active=True)
serializer_class = ProductSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

def get_queryset(self):
"""Filter products based on query parameters."""
queryset = super().get_queryset()
name = self.request.query_params.get('name')
if name:
queryset = queryset.filter(name__icontains=name)
return queryset

def perform_destroy(self, instance):
"""Soft delete: mark as inactive instead of deleting."""
instance.is_active = False
instance.save()

URLs and Routing

URL organization:

# core/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.v1.urls')),
path('auth/', include('auth.urls')),
]

# api/v1/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from auth.views import UserViewSet
from apps.products.views import ProductViewSet

router = DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'products', ProductViewSet)

urlpatterns = [
path('', include(router.urls)),
]

Django Settings

Configuration organization:

# core/settings/base.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent

# Application configuration
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

THIRD_PARTY_APPS = [
'rest_framework',
'corsheaders',
]

LOCAL_APPS = [
'core',
'auth',
'api',
'apps.products',
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

# Django REST Framework configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}

# Database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'project_db'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', ''),
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}

# Internationalization configuration
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

# Custom user model
AUTH_USER_MODEL = 'auth.User'

Django Tests

Test structure and conventions:

# auth/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError

User = get_user_model()

class UserModelTest(TestCase):
"""Tests for the User model."""

def setUp(self):
"""Initial setup for each test."""
self.user_data = {
'email': 'test@example.com',
'first_name': 'Test',
'last_name': 'User',
'password': 'testpass123'
}

def test_create_user_success(self):
"""Test successful user creation."""
user = User.objects.create_user(**self.user_data)

self.assertEqual(user.email, self.user_data['email'])
self.assertEqual(user.first_name, self.user_data['first_name'])
self.assertTrue(user.check_password(self.user_data['password']))
self.assertFalse(user.is_verified)

def test_user_str_representation(self):
"""Test user string representation."""
user = User.objects.create_user(**self.user_data)
expected = f"{user.first_name} {user.last_name} ({user.email})"

self.assertEqual(str(user), expected)

def test_get_full_name(self):
"""Test get_full_name method."""
user = User.objects.create_user(**self.user_data)
expected = f"{user.first_name} {user.last_name}"

self.assertEqual(user.get_full_name(), expected)

# auth/tests/test_views.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth import get_user_model
from django.urls import reverse

User = get_user_model()

class UserViewSetTest(APITestCase):
"""Tests for UserViewSet."""

def setUp(self):
"""Initial setup for each test."""
self.user = User.objects.create_user(
email='test@example.com',
first_name='Test',
last_name='User',
password='testpass123'
)
self.client.force_authenticate(user=self.user)

def test_get_current_user(self):
"""Test endpoint to get current user."""
url = reverse('user-me')
response = self.client.get(url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['email'], self.user.email)ta['email'], self.user.email)

def test_create_user_success(self):
"""Test successful user creation."""
self.client.force_authenticate(user=None) # No authentication
url = reverse('user-list')
data = {
'email': 'newuser@example.com',
'first_name': 'New',
'last_name': 'User',
'password': 'newpass123',
'password_confirm': 'newpass123'
}

response = self.client.post(url, data, format='json')

self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(email=data['email']).exists())

Custom Management Commands

Create management commands:

# core/management/commands/setup_dev_data.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from apps.products.models import Product

User = get_user_model()

class Command(BaseCommand):
"""
Command to set up development data.

Usage: python manage.py setup_dev_data
"""
help = 'Set up initial development data'

def add_arguments(self, parser):
"""Add arguments to the command."""
parser.add_argument(
'--users',
type=int,
default=10,
help='Number of users to create'
)

def handle(self, *args, **options):
"""Execute the command."""
users_count = options['users']

self.stdout.write('Creating development data...')

# Create superuser if it doesn't exist
if not User.objects.filter(is_superuser=True).exists():
User.objects.create_superuser(
email='admin@example.com',
first_name='Admin',
last_name='User',
password='admin123'
)
self.stdout.write(
self.style.SUCCESS('Superuser created: admin@example.com')
)

# Create test users
for i in range(users_count):
User.objects.get_or_create(
email=f'user{i}@example.com',
defaults={
'first_name': f'User{i}',
'last_name': 'Test',
'password': 'testpass123'
}
)

self.stdout.write(
self.style.SUCCESS('Development data configured successfully')
)

This configuration ensures that all Django code follows the framework's best practices and maintains consistency with the standards established by the team.