Code Style
This guide covers coding standards, naming conventions, and best practices for ZineCore2 development.
Python Style Guide
ZineCore2 follows PEP 8 with some project-specific conventions.
Line Length
Maximum 88 characters (Black formatter default).
# Good
def create_zine_from_csv(
csv_path, validate=True, skip_errors=False
):
pass
# Bad (too long)
def create_zine_from_csv(csv_path, validate=True, skip_errors=False, log_errors=True, dry_run=False):
pass
Imports
Order: Standard library → Third party → Local imports
# Good
import os
import sys
from datetime import datetime
from django.db import models
from rest_framework import serializers
from core.models import TimestampedModel
from catalog.utils import validate_zine_id
# Bad (mixed order)
from catalog.utils import validate_zine_id
import os
from django.db import models
Grouping: Separate groups with blank line.
Absolute imports preferred:
# Good
from catalog.models import Zine
# Avoid
from .models import Zine
Naming Conventions
Variables and functions: snake_case
zine_id = "zine_001"
created_at = timezone.now()
def get_zine_by_id(zine_id):
pass
Classes: PascalCase
class Zine(TimestampedModel):
pass
class ZineWriteSerializer(serializers.ModelSerializer):
pass
Constants: UPPER_SNAKE_CASE
DEFAULT_PAGE_SIZE = 25
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
Private/internal: Prefix with underscore
def _internal_helper():
pass
_cache = {}
Django-Specific Conventions
Model Fields
Order:
- ID fields (id, external_id)
- Required fields
- Optional fields
- Timestamps (from TimestampedModel)
class Zine(TimestampedModel):
# ID fields
id = models.BigAutoField(primary_key=True)
zine_id = models.CharField(max_length=255, unique=True)
# Required fields
title = models.CharField(max_length=500)
creator = ArrayField(models.CharField(max_length=255))
# Optional fields
abstract = models.TextField(blank=True)
isbn = models.CharField(max_length=13, blank=True)
# Timestamps inherited from TimestampedModel
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
Field arguments order:
- Field type positional args
- max_length / choices
- blank / null
- default
- help_text
status = models.CharField(
max_length=50,
choices=[('draft', 'Draft'), ('published', 'Published')],
blank=True,
default='draft',
help_text="Publication status"
)
Serializers
Name pattern: {Model}{Read|Write}Serializer
class ZineReadSerializer(serializers.ModelSerializer):
"""Read serializer with nested objects"""
pass
class ZineWriteSerializer(serializers.ModelSerializer):
"""Write serializer with simple IDs"""
pass
Meta class order:
class ZineWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Zine
fields = ['zine_id', 'title', ...]
read_only_fields = ['created_at', 'updated_at']
ViewSets
class ZineViewSet(viewsets.ModelViewSet):
"""
ViewSet for Zine CRUD operations.
Supports:
- List: GET /api/zines/
- Create: POST /api/zines/
- Retrieve: GET /api/zines/{zine_id}/
- Update: PUT/PATCH /api/zines/{zine_id}/
- Delete: DELETE /api/zines/{zine_id}/
"""
queryset = Zine.objects.all()
lookup_field = 'zine_id'
permission_classes = [IsAuthenticatedOrReadOnly]
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return ZineWriteSerializer
return ZineReadSerializer
Documentation
Docstrings
Use Google-style docstrings:
def create_zine(zine_data, validate=True):
"""
Create a new zine record.
Args:
zine_data (dict): Zine data including title, creator, etc.
validate (bool): Whether to validate before saving. Default True.
Returns:
Zine: Created zine instance.
Raises:
ValidationError: If data is invalid and validate=True.
ValueError: If zine_id already exists.
Example:
>>> zine = create_zine({
... 'zine_id': 'zine_001',
... 'title': 'My Zine',
... 'creator': ['agent_001']
... })
"""
pass
Class docstrings:
class Zine(TimestampedModel):
"""
Zine bibliographic record (ZineCore2).
Represents a specific zine issue with metadata following the
ZineCore2 Dublin Core Application Profile.
Attributes:
zine_id (str): External unique identifier.
title (str): Primary title of the zine.
creator (list): Agent IDs of creators.
"""
pass
Function docstrings (simple):
def validate_zine_id(zine_id):
"""Check if zine_id follows naming convention."""
return zine_id.startswith('zine_')
Inline Comments
Use sparingly — code should be self-documenting.
Good use cases:
# Optimization: Cache frequently accessed data
cache = {}
# TODO: Implement bulk update endpoint
# FIXME: Handle edge case with empty arrays
# HACK: Temporary workaround for Django bug #12345
Bad use cases:
# Loop through zines
for zine in zines:
# Print title
print(zine.title) # This prints the title
Code Formatting
Use Black
# Format all files
black .
# Check without modifying
black --check .
# Format specific file
black catalog/models.py
Black configuration in pyproject.toml:
[tool.black]
line-length = 88
target-version = ['py312']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.venv
| migrations
)/
'''
String Formatting
Prefer f-strings:
# Good
message = f"Created zine: {zine.title}"
# Avoid
message = "Created zine: {}".format(zine.title)
message = "Created zine: " + zine.title
Multi-line strings:
# Triple quotes for multi-line
description = """
This is a long description
that spans multiple lines.
"""
# Or explicit line breaks
message = (
"This is a very long message "
"that is split across multiple lines "
"for readability."
)
Type Hints
Use type hints for function signatures:
from typing import List, Dict, Optional
def get_zines_by_subject(
subject_code: str,
limit: Optional[int] = None
) -> List[Zine]:
"""Get zines by subject code."""
queryset = Zine.objects.filter(subject__contains=[subject_code])
if limit:
queryset = queryset[:limit]
return list(queryset)
Return type hints:
def create_zine(data: Dict) -> Zine:
pass
def get_zine_count() -> int:
pass
def process_data() -> None:
pass
Error Handling
Exceptions
Be specific:
# Good
try:
zine = Zine.objects.get(zine_id=zine_id)
except Zine.DoesNotExist:
raise ValueError(f"Zine {zine_id} not found")
# Bad
try:
zine = Zine.objects.get(zine_id=zine_id)
except Exception: # Too broad
pass
Don't swallow exceptions:
# Bad
try:
process_zine(zine)
except Exception:
pass # Silent failure
# Good
try:
process_zine(zine)
except ValidationError as e:
logger.error(f"Validation failed: {e}")
raise
Logging
import logging
logger = logging.getLogger(__name__)
def import_zines(csv_path):
logger.info(f"Starting import from {csv_path}")
try:
# Import logic
logger.debug(f"Processed {count} records")
except Exception as e:
logger.error(f"Import failed: {e}", exc_info=True)
raise
Log levels:
- DEBUG — Detailed information
- INFO — General information
- WARNING — Warning messages
- ERROR — Error messages
- CRITICAL — Critical errors
Testing Code Style
Test Naming
Pattern: test_{what}_{condition}_{expected_result}
class TestZineAPI(APITestCase):
def test_create_zine_valid_data_returns_201(self):
pass
def test_create_zine_missing_title_returns_400(self):
pass
def test_get_zine_nonexistent_id_returns_404(self):
pass
Test Structure
Arrange-Act-Assert:
def test_create_zine(self):
# Arrange
data = {
'zine_id': 'zine_001',
'title': 'Test Zine',
# ...
}
# Act
response = self.client.post('/api/zines/', data)
# Assert
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Test Zine')
Git Commit Messages
Format
<type>: <subject>
<body>
<footer>
Types
- feat — New feature
- fix — Bug fix
- docs — Documentation
- style — Code style (formatting)
- refactor — Code refactoring
- test — Tests
- chore — Maintenance
Examples
Simple:
fix: Prevent 500 error when creator array is empty
With body:
feat: Add bulk delete endpoint for zines
Add DELETE /api/zines/bulk-delete/ endpoint that accepts
an array of zine IDs and deletes them in a single transaction.
Returns count of deleted zines and any errors encountered.
With footer:
fix: Correct validation for empty subject array
Subject array must contain at least one element.
Fixes #123
Guidelines:
- Subject: 50 characters or less
- Imperative mood ("Add" not "Added" or "Adds")
- No period at end of subject
- Body: Wrap at 72 characters
- Blank line between subject and body
Best Practices
DRY (Don't Repeat Yourself)
Extract common code:
# Bad
def get_zine(zine_id):
try:
return Zine.objects.get(zine_id=zine_id)
except Zine.DoesNotExist:
raise ValueError(f"Zine {zine_id} not found")
def get_agent(agent_id):
try:
return Agent.objects.get(agent_id=agent_id)
except Agent.DoesNotExist:
raise ValueError(f"Agent {agent_id} not found")
# Good
def get_object_or_error(model, field, value):
try:
return model.objects.get(**{field: value})
except model.DoesNotExist:
raise ValueError(f"{model.__name__} {value} not found")
# Usage
zine = get_object_or_error(Zine, 'zine_id', zine_id)
agent = get_object_or_error(Agent, 'agent_id', agent_id)
KISS (Keep It Simple)
Prefer simple, readable code:
# Bad (too clever)
zines = [z for z in Zine.objects.all() if z.subject and any(s in z.subject for s in ['feminism', 'punk'])]
# Good (clear)
zines = Zine.objects.filter(
models.Q(subject__contains=['feminism']) |
models.Q(subject__contains=['punk'])
)
Single Responsibility
Each function/class does one thing:
# Bad (does too much)
def process_zine(data):
# Validate
# Save to database
# Send notification
# Update cache
pass
# Good (separate concerns)
def validate_zine(data):
pass
def save_zine(data):
pass
def notify_zine_created(zine):
pass
def update_zine_cache(zine):
pass
Code Review Checklist
- Follows PEP 8
- Formatted with Black
- Type hints on function signatures
- Docstrings on classes and complex functions
- No commented-out code
- Tests added for new functionality
- No hardcoded values (use settings/constants)
- Error handling is appropriate
- Logging added where appropriate
- No security vulnerabilities
Tools
Black (Code Formatter)
pip install black
black .
Flake8 (Linter)
pip install flake8
flake8 .
isort (Import Sorter)
pip install isort
isort .
mypy (Type Checker)
pip install mypy
mypy .
Pre-commit Hooks
pip install pre-commit
pre-commit install
Next Steps
- Testing — Writing and running tests
- Contributing — How to contribute
- Troubleshooting — Common issues