mypy ile Python Type Checking ve Statik Analiz

Python projelerinde bir noktadan sonra “bu fonksiyon ne döndürüyor?” ya da “bu parametreye string mi int mi geçmeliyim?” sorularıyla boğulmaya başlarsınız. Özellikle ekip büyüdükçe, kod tabanı genişledikçe bu belirsizlik ciddi bug’lara dönüşür. İşte tam bu noktada mypy devreye giriyor ve Python’ın dinamik yapısına rağmen statik tip kontrolü imkanı sunuyor.

mypy Nedir ve Neden Kullanmalısınız

mypy, Python kodunuzu çalıştırmadan tip hatalarını tespit eden bir statik analiz aracıdır. Python 3.5 ile gelen tip anotasyon desteğini kullanarak kodunuzu analiz eder ve potansiyel hataları önceden yakalar.

“Python zaten dinamik tipli, neden uğraşayım?” diye düşünebilirsiniz. Ama şunu söyleyeyim: production ortamında AttributeError: 'NoneType' object has no attribute 'strip' hatasıyla karşılaştıktan sonra bu soruyu sormuyorsunuz. mypy bu tür hataları deploy öncesinde yakalar.

CI/CD pipeline’ınıza entegre ettiğinizde mypy, SonarQube veya ESLint’in yaptığına benzer bir rol üstlenir; kod kalitesini otomatik olarak kontrol eden bir katman oluşturur. Farkı şu: mypy tip güvenliğine odaklanır ve Python ekosistemiyle çok daha derin entegre çalışır.

Kurulum ve Temel Konfigürasyon

Kurulum basit:

pip install mypy
# veya proje bağımlılıklarına eklemek için
pip install mypy types-requests types-PyYAML

# Belirli bir dosyayı kontrol et
mypy app.py

# Tüm projeyi kontrol et
mypy src/

# Daha sıkı modda çalıştır
mypy --strict src/

Proje kökünde mypy.ini veya setup.cfg içinde konfigürasyon tanımlamak en iyi pratik. Ben her zaman mypy.ini tercih ederim, daha okunabilir:

# mypy.ini oluştur
cat > mypy.ini << 'EOF'
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
strict_equality = True

# Üçüncü parti kütüphaneler için tip stub'ı yoksa sessiz geç
[mypy-some_library.*]
ignore_missing_imports = True
EOF

Bu konfigürasyonla başlamak biraz ağır gelebilir, özellikle mevcut bir projeye mypy ekliyorsanız. O yüzden kademeli yaklaşımı öneririm.

Temel Tip Anotasyonları

Önce basit örneklerle başlayalım. Tip anotasyonu olmayan kod:

# annotations_before.py - mypy bu kodu analiz edemez
def process_user_data(user_id, include_deleted):
    if include_deleted:
        return get_all_users(user_id)
    return get_active_users(user_id)

def calculate_discount(price, discount_rate):
    return price * (1 - discount_rate)

Anotasyonlar eklendikten sonra:

# annotations_after.py
from typing import Optional, List, Dict, Any
from decimal import Decimal

def process_user_data(
    user_id: int,
    include_deleted: bool = False
) -> List[Dict[str, Any]]:
    if include_deleted:
        return get_all_users(user_id)
    return get_active_users(user_id)

def calculate_discount(
    price: Decimal,
    discount_rate: float
) -> Decimal:
    if not 0 <= discount_rate <= 1:
        raise ValueError(f"Geçersiz indirim oranı: {discount_rate}")
    return price * Decimal(str(1 - discount_rate))

# mypy şimdi şu hatayı yakalar:
# calculate_discount("100", 0.2)  -> Argument 1 has incompatible type "str"; expected "Decimal"

Gerçek Dünya Senaryosu: API İstemcisi

Bir e-ticaret projesinde çalışıyordum ve ödeme servisiyle entegrasyon kodunda sürekli tip kaynaklı hatalar çıkıyordu. İşte bu senaryoyu basitleştirerek göstereyim:

# payment_client.py
from typing import Optional, TypedDict, Union, Literal
from dataclasses import dataclass
from enum import Enum
import requests

class PaymentStatus(Enum):
    PENDING = "pending"
    SUCCESS = "success"
    FAILED = "failed"
    REFUNDED = "refunded"

class PaymentRequest(TypedDict):
    amount: int  # Kuruş cinsinden
    currency: str
    customer_id: str
    description: Optional[str]

@dataclass
class PaymentResponse:
    transaction_id: str
    status: PaymentStatus
    amount: int
    error_message: Optional[str] = None

class PaymentClient:
    def __init__(self, api_key: str, base_url: str) -> None:
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    def charge(self, request: PaymentRequest) -> PaymentResponse:
        try:
            response = self.session.post(
                f"{self.base_url}/charge",
                json=request,
                timeout=30
            )
            response.raise_for_status()
            data = response.json()
            return PaymentResponse(
                transaction_id=data["transaction_id"],
                status=PaymentStatus(data["status"]),
                amount=data["amount"]
            )
        except requests.HTTPError as e:
            return PaymentResponse(
                transaction_id="",
                status=PaymentStatus.FAILED,
                amount=0,
                error_message=str(e)
            )

    def refund(
        self,
        transaction_id: str,
        amount: Optional[int] = None
    ) -> PaymentResponse:
        payload: Dict[str, Union[str, int]] = {
            "transaction_id": transaction_id
        }
        if amount is not None:
            payload["amount"] = amount

        # ... implementasyon devam eder
        ...

Bu kodu mypy ile kontrol ettiğinizde, örneğin charge metoduna yanlış tipte veri geçirmeye çalışırsanız hemen uyarı alırsınız.

Protocol ve Generic Kullanımı

Daha ileri seviye kullanımda Protocol sınıfı çok işe yarıyor. Duck typing’in tip güvenli versiyonu diyebiliriz:

# protocols.py
from typing import Protocol, TypeVar, Generic, Iterator
from contextlib import contextmanager

T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)

class Repository(Protocol[T_co]):
    """Depo deseni için temel protokol."""

    def get_by_id(self, entity_id: int) -> Optional[T_co]:
        ...

    def get_all(self) -> List[T_co]:
        ...

    def save(self, entity: T_co) -> T_co:
        ...

    def delete(self, entity_id: int) -> bool:
        ...

class CacheableRepository(Repository[T_co], Protocol[T_co]):
    """Önbellek destekli depo protokolü."""

    def invalidate_cache(self, entity_id: Optional[int] = None) -> None:
        ...

# Kullanım örneği - mypy bu implementasyonun
# Repository protokolünü karşıladığını doğrular
class UserRepository:
    def __init__(self, db_session: Any) -> None:
        self.db = db_session

    def get_by_id(self, entity_id: int) -> Optional[User]:
        return self.db.query(User).filter(User.id == entity_id).first()

    def get_all(self) -> List[User]:
        return self.db.query(User).all()

    def save(self, entity: User) -> User:
        self.db.add(entity)
        self.db.commit()
        return entity

    def delete(self, entity_id: int) -> bool:
        user = self.get_by_id(entity_id)
        if user is None:
            return False
        self.db.delete(user)
        self.db.commit()
        return True

Kademeli Tip Ekleme Stratejisi

Mevcut bir projeye mypy eklerken “hepsini bir anda tip ekleyeceğim” diye tutturursanız haftalar geçer ve motivasyonunuz biter. Bunun yerine şu stratejiyi izleyin:

# Önce mevcut durumu gör, --ignore-missing-imports ile başla
mypy src/ --ignore-missing-imports 2>&1 | tail -20

# Kaç hata var?
mypy src/ --ignore-missing-imports 2>&1 | grep "error:" | wc -l

# Modül bazlı hata sayısını gör
mypy src/ --ignore-missing-imports 2>&1 | grep "error:" | 
  sed 's/:.*//' | sort | uniq -c | sort -rn | head -20

Sonra mypy.ini‘de kademeli sıkılaştırma yapın:

# mypy.ini - kademeli yaklaşım
cat > mypy.ini << 'EOF'
[mypy]
python_version = 3.11

# Başlangıç: sadece temel kontroller
ignore_missing_imports = True
warn_return_any = False
disallow_untyped_defs = False

# Yeni yazılan modüller için sıkı mod
[mypy-myproject.new_module.*]
disallow_untyped_defs = True
warn_return_any = True
no_implicit_optional = True

# Eski modüller için geçici muafiyet
[mypy-myproject.legacy.*]
ignore_errors = True
EOF

Bu yaklaşımla yeni kod kaliteli, eski kod da yavaş yavaş düzeltilir.

CI/CD Pipeline Entegrasyonu

mypy’ı CI/CD’ye entegre etmek son derece önemli. GitHub Actions için örnek:

# .github/workflows/code-quality.yml
cat > .github/workflows/code-quality.yml << 'EOF'
name: Code Quality

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python Kur
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: "pip"

      - name: Bağımlılıkları Yükle
        run: |
          pip install -r requirements.txt
          pip install mypy types-requests types-PyYAML

      - name: mypy Tip Kontrolü
        run: |
          mypy src/ 
            --config-file mypy.ini 
            --junit-xml reports/mypy-results.xml

      - name: Sonuçları Yükle
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: mypy-results
          path: reports/mypy-results.xml
EOF

Pre-commit hook ile de entegre edebilirsiniz:

# .pre-commit-config.yaml
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.7.0
    hooks:
      - id: mypy
        additional_dependencies:
          - types-requests
          - types-PyYAML
          - types-redis
        args: [--config-file, mypy.ini]
EOF

# Pre-commit'i kur
pip install pre-commit
pre-commit install

mypy ile Birlikte Kullanılacak Araçlar

mypy tek başına güçlüdür ama ekosistemi daha da zenginleştirir:

  • pyright: Microsoft’un type checker’ı, VS Code ile mükemmel entegre çalışır. mypy’dan bazı senaryolarda daha hızlı
  • pylance: VS Code eklentisi olarak pyright’ı kullanır, gerçek zamanlı tip kontrolü sağlar
  • beartype: Runtime tip kontrolü için kullanılır, mypy’ı tamamlar
  • pydantic: Veri doğrulama için mypy ile çok iyi çalışır, API katmanlarında özellikle güçlü
  • pyanalyze: Instagram’ın geliştirdiği alternatif tip kontrolcüsü

SonarQube kullanıyorsanız mypy çıktısını SonarQube’e raporlayabilirsiniz:

# mypy çıktısını sonarqube formatına çevir
pip install mypy2junit

# mypy çalıştır ve JUnit formatında çıktı al
mypy src/ --config-file mypy.ini > mypy_output.txt 2>&1 || true

# sonar-project.properties içinde
cat >> sonar-project.properties << 'EOF'
sonar.python.mypy.reportPaths=reports/mypy-results.xml
EOF

Yaygın Hatalar ve Çözümleri

Ekiplerin mypy’a geçerken en sık takıldığı noktalara değineyim:

Optional kullanımını unutmak: Bir değer None olabiliyorsa bunu açıkça belirtmelisiniz. no_implicit_optional = True ayarıyla mypy sizi uyarır:

# Yanlış - mypy uyarı verir
def get_user(user_id: int) -> User:
    user = db.find(user_id)
    return user  # Burada None dönebilir!

# Doğru
def get_user(user_id: int) -> Optional[User]:
    return db.find(user_id)

# Python 3.10+ için daha temiz sözdizimi
def get_user(user_id: int) -> User | None:
    return db.find(user_id)

cast ve # type: ignore kötüye kullanımı: Bunlar gerektiğinde kullanılacak araçlar, her yere serpiştirilerek değil:

from typing import cast

# cast - tipi biliyorsunuz ama mypy çıkaramıyor
raw_data = json.loads(response.text)
user_data = cast(Dict[str, str], raw_data)

# type: ignore - sadece gerçekten çözümsüzse
result = third_party_function()  # type: ignore[no-untyped-call]

# YANLIŞ kullanım - sorunun üstünü örtmek
def process(data: Any) -> Any:  # Bütün tip güvenliğini kaybettiniz
    return data.some_method()  # type: ignore

Üçüncü parti kütüphane stub’ları:

# Stub yoksa kendiniz yazabilirsiniz
mkdir -p stubs/some_library
cat > stubs/some_library/__init__.pyi << 'EOF'
from typing import Any, Optional

def some_function(arg: str) -> Optional[int]: ...

class SomeClass:
    def __init__(self, config: dict[str, Any]) -> None: ...
    def process(self) -> bool: ...
EOF

# mypy.ini'ye stub yolunu ekle
# [mypy]
# mypy_path = stubs

Performans İpuçları

Büyük projelerde mypy yavaşlayabilir. Hızlandırmak için:

# Daemon modunda çalıştır - ilk çalıştırma yavaş,
# sonrakiler çok hızlı
dmypy run -- src/ --config-file mypy.ini

# Daemon durumunu kontrol et
dmypy status

# Cache kullan (varsayılan açık, ama temizlemek gerekebilir)
mypy src/ --cache-dir .mypy_cache

# Paralel çalıştırma (mypy 0.990+)
mypy src/ -j 4

# Sadece değişen dosyaları kontrol et
mypy src/ --incremental

Ekip Adaptasyonu

Teknik kısım kolay, asıl zor olan ekibi ikna etmek. Deneyimlerime göre şu yaklaşım işe yarıyor:

  • Küçük başlayın: Sadece yeni dosyalar için tip zorunluluğu koyun
  • Araçları kolaylaştırın: IDE entegrasyonunu kurun, geliştiriciler gerçek zamanlı feedback görünce ikna olur
  • Hata örnekleri toplayın: Geçmişte yaşanan ve tip kontrolüyle önlenebilecek bug’ları gösterin
  • Build’i kırmayın aniden: Önce warning, sonra error moduna geçin
  • Code review’da zorunlu kılın: “Bu fonksiyonun dönüş tipi nedir?” sorusu PR’larda standart hale gelsin

Türkiye’deki projelerde çalışırken gördüğüm en yaygın direniş “zaman kaybı” algısı. Buna karşı argümanım şu: tip anotasyonu yazmak 5 dakika alır, production’da bir None hatası debug etmek 5 saat alabilir.

Sonuç

mypy, Python projelerinde kod kalitesini ciddi ölçüde artıran bir araç. Sıfırdan başlayan bir projede --strict modunu aktif etmenizi öneririm; disiplin yaratır. Mevcut projelerde ise kademeli yaklaşım kaçınılmaz.

SonarQube veya ESLint gibi araçların yaptığı kalite kapısı rolünü Python dünyasında mypy üstleniyor. CI/CD pipeline’ınıza entegre ettiğinizde, tip kaynaklı bug’ların production’a ulaşma ihtimali dramatik biçimde düşüyor.

Son bir öneri: mypy’ı tek başına düşünmeyin. black ile kod formatı, ruff veya flake8 ile linting, mypy ile tip kontrolü ve pytest ile testleri birlikte kullandığınızda Python projenizin kalitesi kurumsal düzeye çıkıyor. Bu araç zinciri, Python’ın dinamik doğasından gelen riskleri minimize ederken dili sevdiren esnekliği de koruyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir