Type-Safe Python Tests in the Age of AI

While AI coding assistants boost productivity, they also amplify risks like code duplication and architectural decay. A modern testing strategy is the essential safety net.

Test the Contract

Focus tests on the code's observable behavior, not its internal implementation.

# service.py
def add(a: int, b: int) -> int:
    return a + b

# test_service.py
def test_add_positive_numbers():
    assert add(2, 3) == 5

Decouple with DI

Use Dependency Injection to provide dependencies from the outside for easier testing.

class Notifier: ...

class OrderService:
    def __init__(self, notifier: Notifier):
        self._notifier = notifier

    def place_order(self, order):
        # ... logic ...
        self._notifier.send("Order placed!")

Honest Test Doubles

Use fakes that honor the real object's contract to avoid silent breakages.

class FakeNotifier(Notifier):
    def __init__(self):
        self.sent_messages = []
    
    def send(self, message: str):
        self.sent_messages.append(message)

# In test:
notifier = FakeNotifier()
service = OrderService(notifier)

Static Contracts

Use `typing.Protocol` to define explicit interfaces that type checkers can enforce.

from typing import Protocol

class CanNotify(Protocol):
    def send(self, message: str) -> None: ...

# Mypy will error if send() is missing
class EmailNotifier:
    def send(self, message: str): ...

Architectural Boundaries

Codify architectural rules as tests to prevent structural erosion over time.

from pytest_arch import archrule

def test_domain_is_isolated():
    rule = (
        archrule("Domain Layer")
        .should_not_import("app.services")
        .should_not_import("app.repositories")
    )
    rule.check("app.domain")

Recommended Python Quality Stack

Pytest

Test Runner

Ruff

Linter & Formatter

Mypy/Pyright

Type Checker

PyTestArch

Architecture Tests

Coverage.py

Code Coverage