Python Best Practices

Practical patterns for writing clean, maintainable Python — especially in Django projects.

Use Type Hints

Type hints catch bugs before runtime and improve editor support:

from typing import Optional

def get_post_by_slug(slug: str) -> Optional["Post"]:
    try:
        return Post.objects.get(slug=slug)
    except Post.DoesNotExist:
        return None

For Django models, use __future__ annotations to avoid circular imports:

from __future__ import annotations
from django.db import models

class Doc(models.Model):
    subject: models.ForeignKey[Subject]

Prefer f-strings

# Good
message = f"Created {count} posts in {elapsed:.2f}s"

# Avoid
message = "Created {} posts in {:.2f}s".format(count, elapsed)
message = "Created %d posts in %.2fs" % (count, elapsed)

Context Managers for Resources

from contextlib import contextmanager

@contextmanager
def timer(label: str):
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"{label}: {elapsed:.2f}s")

# Usage
with timer("Database seed"):
    seed_all_content()

Dataclasses Over Dicts

When you're passing structured data around, use dataclasses:

from dataclasses import dataclass

@dataclass
class SeedResult:
    created: int
    updated: int
    errors: list[str]

    @property
    def total(self) -> int:
        return self.created + self.updated

Path Handling with pathlib

from pathlib import Path

content_dir = Path("blog/content")
for md_file in content_dir.glob("*.md"):
    text = md_file.read_text(encoding="utf-8")
    slug = md_file.stem  # filename without extension

Enumerated Choices

Django's TextChoices and IntegerChoices are cleaner than raw constants:

class Post(models.Model):
    class Status(models.IntegerChoices):
        DRAFT = 0, "Draft"
        PUBLISHED = 1, "Published"

    status = models.IntegerField(choices=Status.choices, default=Status.DRAFT)

# Usage
Post.objects.filter(status=Post.Status.PUBLISHED)

Guard Clauses

Flatten nested logic with early returns:

# Instead of deeply nested if/else
def process_request(request):
    if not request.user.is_authenticated:
        return redirect("login")
    if request.method != "POST":
        return render(request, "form.html")
    if not form.is_valid():
        return render(request, "form.html", {"form": form})

    # Happy path — no nesting
    form.save()
    return redirect("success")