Python Code Development#

You are here

Contributing Guide → Python Code Development

This page covers quality gates and development practices for FairDM framework contributions. If you landed here from a search, start with the Contributing Guide overview to understand the framework contributor role and Before You Start for environment setup.

This guide covers the quality gates and development practices for contributing Python code to FairDM. All contributions must pass these quality checks before being merged.

Quality Gates Overview#

FairDM enforces four core quality gates to maintain code quality, consistency, and documentation:

  1. Tests: All code must have passing unit tests

  2. Type Checking: Code must pass mypy type checking

  3. Linting: Code must pass ruff linting checks

  4. Documentation Build: Documentation must build without errors

Important

Before submitting a pull request, run all quality gates locally to ensure your changes will pass CI checks.

1. Running Tests#

FairDM uses pytest for testing. The framework’s tests are in tests/ and the reference application’s are in demo/tests/. Both run by default: the demo app ships with FairDM, so a change that breaks it is a change that breaks a worked example every portal developer reads.

Run All Tests#

uv run pytest

Tests run in parallel by default, one worker per available core, with each test class or module kept together on a single worker. Every worker builds its own test database, so tests running at the same time cannot see each other’s data.

Parallel output interleaves, which makes a single failure harder to read. Add -n0 to any command to fall back to one process:

uv run pytest tests/test_core/test_models.py -n0

Run Tests for a Specific Module#

uv run pytest tests/test_core/

Run a Specific Test File#

uv run pytest tests/test_core/test_models.py

Run a Specific Test Function#

uv run pytest tests/test_core/test_models.py::test_project_creation

Run Tests with Coverage#

uv run pytest --cov=fairdm --cov-report=html

This generates a coverage report in htmlcov/index.html showing which lines are tested.

Tip

Test-Driven Development: Write tests before implementing features. This helps clarify requirements and ensures your code is testable.

Writing Good Tests#

  • Test one thing per test: Each test should verify a single behavior

  • Use descriptive names: test_project_requires_title is better than test_project1

  • Arrange-Act-Assert: Structure tests clearly (setup → execute → verify)

  • Use fixtures: Leverage pytest fixtures for common setup (see tests/conftest.py)

  • Test edge cases: Don’t just test the happy path

Example Test:

import pytest
from fairdm.core.models import Project

def test_project_requires_title():
    """Project creation should fail without a title."""
    with pytest.raises(ValueError):
        Project.objects.create(title="", description="Test project")

def test_project_str_returns_title():
    """Project string representation should return its title."""
    project = Project.objects.create(title="My Project", description="Test")
    assert str(project) == "My Project"

2. Type Checking with mypy#

FairDM uses mypy to catch type errors before runtime.

Run mypy#

uv run mypy fairdm

Type-checking configuration#

mypy is configured in pyproject.toml:

[tool.mypy]
python_version = "3.10"
plugins = ["mypy_django_plugin.main"]

Writing Type-Safe Code#

  • Add type hints to function signatures:

    def create_sample(name: str, dataset_id: int) -> Sample:
        return Sample.objects.create(name=name, dataset_id=dataset_id)
    
  • Use Optional for nullable fields:

    from typing import Optional
    
    def get_project(project_id: int) -> Optional[Project]:
        try:
            return Project.objects.get(id=project_id)
        except Project.DoesNotExist:
            return None
    
  • Type Django querysets:

    from django.db.models import QuerySet
    
    def get_active_projects() -> QuerySet[Project]:
        return Project.objects.filter(status="active")
    

Tip

Use django-stubs: FairDM includes django-stubs for accurate Django type hints. If mypy complains about Django APIs, check the django-stubs documentation.

3. Linting with Ruff#

FairDM uses ruff for fast Python linting and code formatting.

Run Ruff Linter#

uv run ruff check fairdm

Auto-Fix Issues#

Many linting issues can be automatically fixed:

uv run ruff check --fix fairdm

Linting configuration#

Ruff is configured in pyproject.toml:

[tool.ruff]
line-length = 120
select = ["E", "F", "I", "N", "W", "C90", "UP", "B", "A", "C4", "DJ", "RUF"]
ignore = ["E501", "E731", "RUF012", "TRY003", "A003", "F403", "F405", "C901"]

Common Linting Rules#

  • E/F: PEP 8 style and syntax errors

  • I: Import sorting (isort-compatible)

  • N: PEP 8 naming conventions

  • B: Bugbear (common Python bugs and design problems)

  • DJ: Django-specific linting rules

  • UP: Upgrade syntax for newer Python versions

Import Sorting#

Ruff automatically sorts imports. The expected order is:

  1. Standard library imports

  2. Third-party imports (Django, etc.)

  3. Local application imports

Example:

import os
from pathlib import Path

from django.db import models
from django.contrib.auth import get_user_model

from fairdm.core.models import Project
from fairdm.utils import generate_slug

4. Building Documentation#

FairDM uses Sphinx with the pydata-sphinx-theme for documentation.

Build Documentation Locally#

uv run sphinx-build -b html docs docs/_build/html

Open docs/_build/html/index.html in your browser to view the built documentation.

Check for Documentation Errors#

uv run sphinx-build -W -b html docs docs/_build/html

The -W flag treats warnings as errors, ensuring all documentation issues are caught.

Live Documentation Server#

For live reloading while editing docs:

uv run sphinx-autobuild docs docs/_build/html

Open http://localhost:8000 to view live documentation updates.

Writing Good Documentation#

  • Use Markdown: FairDM docs use MyST Markdown (.md files)

  • Follow the style guide: See Documentation Style Guide for formatting conventions

  • Include code examples: Show users how to use features, not just what they do

  • Cross-reference liberally: Link to related pages and API docs

  • Use directives: Leverage Sphinx directives like {tip}, {warning}, {seealso} for clarity

Example Documentation:

# Registering Custom Models

To register a custom Sample model with the FairDM registry:

```python
from fairdm.registry import registry, SampleConfig
from myapp.models import RockSample

registry.register(
    RockSample,
    config=SampleConfig(
        list_fields=["name", "collection_date", "rock_type"],
        filter_fields=["rock_type", "collection_date"]
    )
)
```

```{tip}
If you don't provide a `SampleConfig`, FairDM will auto-generate sensible defaults based on your model fields.
```

```{seealso}
For advanced registry customization, see [Registry API Reference](../api/registry.md).
```

Running All Quality Gates at Once#

To run all quality gates in one command:

uv run pytest && \
uv run mypy fairdm && \
uv run ruff check fairdm && \
uv run sphinx-build -W -b html docs docs/_build/html

If all commands succeed, your code is ready for a pull request.

Tip

Use pre-commit hooks: Install pre-commit hooks to automatically run linting and type checking before each commit:

uv run pre-commit install

This catches issues early and prevents CI failures.


## Code Style Guidelines

### General Python Style

- **Maximum line length**: 120 characters
- **Use f-strings**: Prefer `f"Hello {name}"` over `"Hello {}".format(name)` or `"Hello %s" % name`
- **Use pathlib**: Prefer `Path` over `os.path` for file operations
- **Avoid bare except**: Always catch specific exceptions

**Good**:

```python
from pathlib import Path

def read_config(config_path: Path) -> dict:
    try:
        with config_path.open() as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

Bad:

import os

def read_config(config_path):
    try:
        with open(config_path) as f:
            return json.load(f)
    except:
        return {}

Django-Specific Style#

  • Fat models, thin views: Business logic belongs in models, managers, or services—not views

  • Use Django ORM efficiently: Leverage select_related() and prefetch_related() to avoid N+1 queries

  • Follow Django conventions: Use get_absolute_url(), __str__(), Meta classes, etc.

  • Use lifecycle hooks: Prefer django-lifecycle over signals for model state transitions

Example:

from django.db import models
from django_lifecycle import LifecycleModel, hook, AFTER_CREATE

class Sample(LifecycleModel):
    name = models.CharField(max_length=255)
    dataset = models.ForeignKey("Dataset", on_delete=models.CASCADE)

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

    def get_absolute_url(self) -> str:
        return reverse("sample-detail", kwargs={"pk": self.pk})

    @hook(AFTER_CREATE)
    def log_creation(self):
        """Log sample creation for audit trail."""
        logger.info(f"Sample {self.name} created in dataset {self.dataset.title}")

Data Migrations#

A RunPython step is handed the database it is meant to operate on, as schema_editor.connection.alias. Every query it makes has to be routed there. A query that leaves the routing out goes to default instead, which is a different database with a different schema in any portal that migrates a second one, so the migration either fails on a column that does not exist there or quietly reads and writes the wrong data.

Good:

def backfill(apps, schema_editor):
    Organization = apps.get_model("contributors", "Organization")
    db_alias = schema_editor.connection.alias

    for org in Organization.objects.using(db_alias).filter(country__isnull=True):
        org.country = "DE"
        org.save(using=db_alias, update_fields=["country"])

Bad:

def backfill(apps, schema_editor):
    Organization = apps.get_model("contributors", "Organization")

    for org in Organization.objects.filter(country__isnull=True):
        org.country = "DE"
        org.save(update_fields=["country"])

tests/test_migrations.py fails the suite when a data migration reads the wrong database, so this is checked rather than remembered.

Next Steps#

See also

Frontend Development: If you’re working on templates or JavaScript, see Frontend Development Guide for additional quality gates (djlint, ESLint, etc.).