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.
