| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
"Kod yalnızca makine için değil, onu okuyacak diğer insanlar için de yazılır."
Bu rehber, Python'da temiz, okunabilir ve sürdürülebilir kod yazma alışkanlıklarını geliştirmek için hazırlanmıştır. Amaç; kodu yalnızca çalışan bir yapı değil, ekip içinde anlaşılır, bakımı ucuz ve hataya daha az açık hale getirmektir.
Her bölüm bir prensibi açıklar, neden önemli olduğunu söyler, kötü/iyi örneklerle somutlaştırır ve sık yapılan sapmaları işaret eder. Örnekler Python 3.10+ sözdizimini kullanır (list[str], X | None, match). Bugün yeni bir proje açıyorsanız hedef sürüm olarak 3.12 veya 3.13 düşünmek makuldür; 3.8 ve 3.9 kullanım ömrünü doldurmuştur.
📌 Sürüm: v2.0.0 — Son güncelleme: 22 Ağustos 2026 🔗 Tüm sürüm geçmişi: CHANGELOG.md 🤝 Katkı için: CONTRIBUTING.md
Bu rehber ne değildir? Dil öğreticisi, algoritma kitabı veya framework dokümantasyonu değildir. FastAPI, Django veya asyncio'yu sıfırdan öğretmez; onları kirletmeden nasıl kullanacağınızı konuşur.
Temiz kod, "güzel görünen kod" değildir. Temiz kod; bir sonraki okuyan kişinin (çoğu zaman altı ay sonraki sizin) doğru değişikliği güvenle yapabildiği koddur. Kirli kod da çalışır. Fark, ikinci özelliği eklediğinizde, üçüncü kişiyi işe aldığınızda ve gece yarısı üretim hatasını avladığınızda ortaya çıkar.
Kirli kodun faturası hemen kesilmez. İlk hafta "hızlı teslim" gibi görünür. Üçüncü ayda her değişiklik yan etki üretir, kimse o dosyaya dokunmak istemez, yeni işe alınan kişi ilk ayını keşifte geçirir. Buna teknik borç denir: faiz işleten, görünmeyen bir kredi.
Python belirli bir yazım felsefesine dayanır. Bu felsefe PEP 20 olarak kayıtlıdır ve yorumlayıcının içine gömülüdür:
>>> import thisTim Peters'ın 19 özdeyişi (yirmincisi kasıtlı olarak yazılmamıştır):
| Özgün metin | Türkçe karşılık |
|---|---|
| Beautiful is better than ugly. | Güzel olan, çirkin olandan iyidir. |
| Explicit is better than implicit. | Açık olan, örtük olandan iyidir. |
| Simple is better than complex. | Basit olan, karmaşık olandan iyidir. |
| Complex is better than complicated. | Karmaşık olan, anlaşılmaz olandan iyidir. |
| Flat is better than nested. | Düz olan, iç içe geçmiş olandan iyidir. |
| Sparse is better than dense. | Seyrek olan, yoğun olandan iyidir. |
| Readability counts. | Okunabilirlik önemlidir. |
| Special cases aren't special enough to break the rules. | Özel durumlar, kuralları bozacak kadar özel değildir. |
| Although practicality beats purity. | Yine de pratiklik, saflığı yener. |
| Errors should never pass silently. | Hatalar asla sessizce geçmemelidir. |
| Unless explicitly silenced. | Açıkça susturulmadıkça. |
| In the face of ambiguity, refuse the temptation to guess. | Belirsizlik karşısında tahmin etme dürtüsüne direnin. |
| There should be one—and preferably only one—obvious way to do it. | Bir işi yapmanın — tercihen yalnızca bir — bariz yolu olmalıdır. |
| Although that way may not be obvious at first unless you're Dutch. | Bu yol, Hollandalı olmadığınız sürece ilk bakışta bariz olmayabilir. |
| Now is better than never. | Şimdi, hiç yapmamaktan iyidir. |
| Although never is often better than right now. | Yine de hiç yapmamak, çoğu zaman hemen şimdi yapmaktan iyidir. |
| If the implementation is hard to explain, it's a bad idea. | Uygulamayı açıklamak zorsa, kötü bir fikirdir. |
| If the implementation is easy to explain, it may be a good idea. | Uygulamayı açıklamak kolaysa, iyi bir fikir olabilir. |
| Namespaces are one honking great idea — let's do more of those! | İsim uzayları harika bir fikirdir — daha fazla kullanalım! |
Bu çeviriler resmi değildir; resmi metin İngilizcedir. Rehber boyunca bu ilkeler somut kararlara dönüşecektir: sessiz except, örtük global durum, iç içe if kuleleri, "zaten benzer" diye acele soyutlama.
Clean Code yalnızca Python'a ait değildir; yazılım mühendisliğinin ortak disiplinidir. Python'un sözdizimi sade, girinti zorunlu, standart kütüphane geniştir. Bu, temiz kodu otomatik yapmaz. Java tarzı sınıf ormanları, sessiz hatalar ve üç harfli değişkenler Python'da da yazılır. Dil size izin verir; disiplin size kalır.
Küçük, güvenli temizlikler birikince mimari değişmiş gibi durur. Büyük "mükemmel rewrite"ler çoğu zaman yarıda kalır. Tercih: testle korunan küçük adımlar.
Kodun en ucuz dokümantasyonu isimdir. Yanlış isim, doğru yorumdan daha çok zarar verir: yorumu atlarız, isme güveniriz.
| Tür | Biçim | Örnek |
|---|---|---|
| Değişken, fonksiyon, metot, modül | snake_case | calculate_total, user_service.py |
| Sınıf, istisna, TypeAlias | PascalCase | Invoice, UserNotFound |
| Sabit | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Korunan üye | tek alt çizgi | _cache |
| Ad-mangling (çok seyrek) | çift alt çizgi | __tokens |
| Dil ile çakışan isim | sona alt çizgi | type_, class_ |
"Özel" metot için çift alt çizgiyi (__foo) refleksle kullanmayın. Çoğu zaman _foo yeterlidir. __foo isim karıştırması (name mangling) üretir; kalıtımda sürpriz yapar.
d = 100
t = 20
tp = d + tproduct_price = 100
tax_amount = 20
total_price = product_price + tax_amountBağlamsız x, y, data1, temp2 yalnızca gerçekten kısa ömürlü, yerel ve matematiksel bir yerde (ör. 3 satırlık bir dönüşüm) kabul edilebilir. Döngü sayacı olarak i, j gelenekseldir; ama "kullanıcılar arasında geziyorum" diyorsanız for user in users: daha doğrudur.
data, value, info, result, obj, item gibi jenerik isimler bir kez kullanılınca her şey data2 olur. raw_payload, discounted_price, active_users tercih edin.
Boolean bir soru gibi okunmalıdır: is_, has_, can_, should_, allows_.
is_active = True
has_verified_email = False
can_retry = remaining_attempts > 0Negatif isimden kaçının. is_not_empty çifte olumsuzluk üretir:
# ❌ Zihinsel takla
if not is_not_empty(items):
...
# ✅ Olumlu ad, doğal olumsuzlama
if is_empty(items):
...
# ✅ Daha iyisi: Python'un doğruluk değerine güvenmek
if not items:
..."is_empty == False daha açık olur" düşüncesi yanlıştır. == False / == True hem gürültüdür hem de __bool__ / __len__ sözleşmesini yok sayar. if items: ve if not items: yeterlidir. Gerçek bir None / boş ayrımı varsa if items is None: kullanın.
user = fetch_user(user_id)
users = fetch_active_users()
user_by_id = {user.id: user for user in users}user_list, user_dict gibi tipi isme gömen adlar, tipi değiştirdiğinizde yalan söyler. Tipi zaten ipucu söyler.
# ❌
def do(u):
...
# ✅
def activate_user(user: User) -> User:
...MAX_LOGIN_ATTEMPTS = 5
DEFAULT_PAGE_SIZE = 20
VAT_RATE = Decimal("0.20")tmp, foo, bar üretim koduna sızmamalıdır. Taslakta kalabilir; PR'da kalamaz.
# ❌
n = 0 # aktif kullanıcı sayısı
# ✅
active_user_count = 0Geliştirici, yorum satırı olmadan da kodu okuyabilmelidir. Yorum hâlâ gerekiyorsa önce ismi düzeltin; yetmezse bölüm 6'ya bakın.
Fonksiyon, kodun cümlesidir. Bir fonksiyon tek bir gerekçeyle değişmelidir. Bu, Single Responsibility Principle (SRP)'dir. "Tek bir iş" demek "tek bir satır" demek değildir; tek bir seviyedeki tek bir görev demektir.
Aşağıdaki fonksiyon üç işi aynı seviyede karıştırır: doğrulama, kalıcılık, bildirim.
def process_user(user: dict) -> None:
if not user.get("email"):
raise ValueError("No email")
db.save(user)
send_welcome_email(user["email"])def validate_user(user: dict) -> None:
if not user.get("email"):
raise ValueError("e-posta zorunludur")
def save_user(user: dict) -> None:
db.save(user)
def notify_user(email: str) -> None:
send_welcome_email(email)
def register_user(user: dict) -> None:
validate_user(user)
save_user(user)
notify_user(user["email"])register_user "hâlâ üç iş yapıyor" gibi durabilir. Yapmaz: kayıt akışını yönetir. Alt adımlar ayrı test edilir, ayrı değişir. SMTP değişince notify_user değişir; şema değişince validate_user değişir.
Dikkat: her satırı fonksiyona çıkarmak da temizlik değildir. Üç satırlık, bir kez kullanılan, ismi gövdesinden uzun bir sarmalayıcı gürültüdür. Kural: yeniden kullanım, test veya okunabilirlik kazanıyorsanız bölün.
# ❌
def create_invoice(
customer_id: int,
currency: str,
due_days: int,
notes: str,
vat_rate: float,
send_copy: bool,
) -> None:
...
# ✅
@dataclass(frozen=True)
class InvoiceDraft:
customer_id: int
currency: str
due_days: int
notes: str
vat_rate: Decimal
send_copy: bool = False
def create_invoice(draft: InvoiceDraft) -> Invoice:
...Mutlu yolu sağa, sapmaları erken return / raise ile yukarı alın. Okuyan kişi önce "ne zaman vazgeçiyoruz", sonra "asıl iş"i görür.
# ❌ İç içe
def withdraw(account: Account, amount: Decimal) -> None:
if account.is_active:
if amount > 0:
if account.balance >= amount:
account.balance -= amount
else:
raise InsufficientFunds(account.id)
else:
raise ValueError("tutar pozitif olmalı")
else:
raise InactiveAccount(account.id)
# ✅ Düz
def withdraw(account: Account, amount: Decimal) -> None:
if not account.is_active:
raise InactiveAccount(account.id)
if amount <= 0:
raise ValueError("tutar pozitif olmalı")
if account.balance < amount:
raise InsufficientFunds(account.id)
account.balance -= amountAynı fonksiyon bazen User, bazen None, bazen False döndürmesin.
# ❌
def find_user(user_id: int):
user = db.get(user_id)
if user is None:
return False
return user
# ✅ Ya nesne ya yokluk
def find_user(user_id: int) -> User | None:
return db.get(user_id)
# ✅ Ya da "olmak zorunda" sözleşmesi
def get_user(user_id: int) -> User:
user = db.get(user_id)
if user is None:
raise UserNotFound(user_id)
return userfind_* yoksa None dönebilir. get_* yoksa istisna fırlatır. Ekip bu sözleşmede anlaşırsa çağrı yerleri tahmin edilebilir olur.
# ❌ İsim okuma vaat ediyor, gövde yazıyor
def get_settings() -> dict:
settings = load_settings()
settings["last_read_at"] = utcnow()
save_settings(settings)
return settings
# ✅
def load_settings() -> dict:
return _read_settings()
def mark_settings_read(settings: dict) -> dict:
updated = {**settings, "last_read_at": utcnow()}
save_settings(updated)
return updatedTek sorumluluklu fonksiyonlar sahte (mock) nesne ormanına ihtiyaç duymaz. Büyük fonksiyonlar kendiliğinden büyümez; izin verdiğiniz için büyür. Bugün 40 satır, review'suz üç ay sonra 200 satırdır.
Karmaşıklık satır sayısı değildir. Döngüsel karmaşıklık (cyclomatic complexity), bağımsız yol sayısıdır: her if, elif, for, and/or, except bir yol ekler. Yol çoğaldıkça test matrisi ve insan belleği şişer.
Koruma cümleleri yalnızca fonksiyon girişinde değil, döngü içinde de geçerlidir: continue ve break bazen iç içe if'ten daha okunur.
# ❌
for user in users:
if user.is_active:
if user.email:
send(user.email)
# ✅
for user in users:
if not user.is_active:
continue
if not user.email:
continue
send(user.email)# ❌ Büyümeye açık kule
def tax_rate(country: str) -> Decimal:
if country == "TR":
return Decimal("0.20")
elif country == "DE":
return Decimal("0.19")
elif country == "US":
return Decimal("0.00")
else:
raise UnknownCountry(country)
# ✅ Veri olarak tarif
TAX_RATE_BY_COUNTRY = {
"TR": Decimal("0.20"),
"DE": Decimal("0.19"),
"US": Decimal("0.00"),
}
def tax_rate(country: str) -> Decimal:
try:
return TAX_RATE_BY_COUNTRY[country]
except KeyError:
raise UnknownCountry(country) from NoneDavranış dallanıyorsa (yalnızca veri değil) match veya strateji nesneleri daha doğrudur:
def label_http_status(status: int) -> str:
match status:
case 200 | 201 | 204:
return "success"
case 401 | 403:
return "auth"
case 429:
return "rate_limited"
case code if 500 <= code < 600:
return "server"
case _:
return "other"match her if zincirinin yerine geçmez. Basit iki dallı kontrol için if daha dürüsttür.
# ❌
if user.age >= 18 and user.has_verified_email and not user.is_banned:
grant_access(user)
# ✅
is_eligible = (
user.age >= 18
and user.has_verified_email
and not user.is_banned
)
if is_eligible:
grant_access(user)# ❌
if user.is_active == True:
...
# ✅
if user.is_active:
...None karşılaştırması istisnadır: kimlik (is) kullanın.
if user is None:
...
if result is not None:
...if user == None: hem yavaştır hem de __eq__ tuhaflıklarına açıktır. Ruff/flake8 bunu E711 olarak işaretler.
Fonksiyonun başarısızlığını -1, "" veya {} ile duyurmayın. Bunlar geçerli veri olabilir. None, istisna veya açık bir sonuç tipi (Ok/Err, bir Result dataclass'ı) kullanın.
Kısaltmalar birbirini dengeler. Birini tapınağa çevirmek diğerlerini ihlal eder.
DRY, metnin tekrarını değil, bilginin tekrarını yasaklar. Aynı iş kuralı iki yerde yaşıyorsa, birini güncelleyip diğerini unutursunuz.
def price_with_vat_tr(net: Decimal) -> Decimal:
return net * Decimal("1.20")
def invoice_vat_tr(net: Decimal) -> Decimal:
return net * Decimal("0.20")Buradaki bilgi "Türkiye KDV oranı %20"dir. İki fonksiyon, iki sihirli sayı, bir yasa değişikliğinde iki unutma noktası.
VAT_RATE_TR = Decimal("0.20")
def vat_amount(net: Decimal, rate: Decimal = VAT_RATE_TR) -> Decimal:
return net * rate
def price_with_vat(net: Decimal, rate: Decimal = VAT_RATE_TR) -> Decimal:
return net + vat_amount(net, rate)Aynı print("User created") satırını iki kez yazmak teknik olarak tekrardır ama bilgi tekrarı değildir. Bir log satırını sabite çekmek bazen okumayı zorlaştırır. Sabite çekmeye değer olan, anlamı olan tekrardır.
İki blok benzer diye tek fonksiyona zorlanmamalıdır.
# ❌ Anlamı silen soyutlama
def create_entity(entity):
db.insert(entity)
logger.info("Entity created")User ile Admin aynı "entity" değildir. Ortak olan kalıcılık + log ise onu açıkça, dar bir yardımcı olarak çıkarın; alan adlarını yok etmeyin:
def persist_and_log(record: object, *, kind: str) -> None:
db.insert(record)
logger.info("%s created", kind)
def create_user(user: User) -> None:
persist_and_log(user, kind="user")
def create_admin(admin: Admin) -> None:
persist_and_log(admin, kind="admin")Hâlâ iki fonksiyon vardır; çünkü iki kavram vardır. Paylaşılan teknik adım tekleşmiştir.
Üç kuralı (Rule of Three): ilk kopya kabul, ikinci kopyada şüphelen, üçüncüde soyutla. İki benzer satır için sınıf hiyerarşisi kurmayın.
En basit doğru çözüm kazansın. "İleride lazım olur" diye eklenen strateji deseni, event bus ve eklenti API'si, çoğu zaman ileride yük olur.
# ❌ Bir toplam için sınıf ormanı
class AbstractCalculator(ABC):
@abstractmethod
def calculate(self, a: int, b: int) -> int: ...
class AddCalculator(AbstractCalculator):
def calculate(self, a: int, b: int) -> int:
return a + b
# ✅
def add(a: int, b: int) -> int:
return a + bBugün ihtiyaç olmayan soyutlamayı yazmayın. Kullanılmayan parametre, boş Base* sınıfı, "ileride mikroservis olur" diye konulan gereksiz arayüz — bunlar YAGNI ihlalidir.
Zen bunu zaten söyler: never is often better than right now.
Sandi Metz'in uyarısı: acele DRY, acele sınıf, acele "generic helper". Önce somut, tekrar eden, anlaşılan kod; sonra soyutlama. AHA, DRY'nin frenidir.
| İlke | Soru |
|---|---|
| DRY | Bu bilgi başka nerede yaşıyor? |
| KISS | Daha sade bir doğru çözüm var mı? |
| YAGNI | Bunu bugün biri kullanıyor mu? |
| AHA | Soyutladığım şey gerçekten aynı kavram mı, yoksa yalnızca benzer mi? |
Temiz kodda yorum, kodun yetersizliğini örtmez; kodun söyleyemediği bağlamı taşır.
# ❌ Fonksiyon zaten söylüyor
def delete(u):
"""kullanıcıyı siler"""
db.delete(u)
# ✅
def delete_user(user: User) -> None:
db.delete(user)# Ödeme sağlayıcı 2. denemeden önce 1500 ms istiyor; aksi halde
# idempotency anahtarı henüz yerleşmemiş oluyor (destek #4821).
time.sleep(1.5)Bu bilgi koddan çıkmaz. Sağlayıcı belgesi, ticket numarası, yasal kısıt, bilinçli teknik borç — bunlar yoruma aittir.
# TODO: stok rezervasyonu ile ödemeyi tek işlemde birleştir (issue #128)
# FIXME: UTC varsayıyoruz; kullanıcı TZ'si henüz yok
# HACK: üçüncü parti SDK thread-safe değil, kilidi burada tutuyoruzTODO/FIXME bir iş kuyruğudur. Sahipsiz TODO, yorum kılığına girmiş borçtur. Issue numarası koyun veya silin.
PEP 257 docstring'i tanımlar. Modül, herkese açık sınıf ve herkese açık fonksiyon bir docstring hak eder. Tek satırlık, bariz bir sarmalayıcıya üç paragraflık Google-style roman yazmayın.
def split_full_name(full_name: str) -> tuple[str, str]:
"""Ad ve soyadı, son boşluktan ayırarak döndürür.
Kurumsal dizinde soyad her zaman son tokendir. Ortadaki
ikinci adlar adın parçası sayılır.
Raises:
ValueError: `full_name` boşsa veya boşluk içermiyorsa.
"""
parts = full_name.strip().split()
if len(parts) < 2:
raise ValueError("ad ve soyad gerekli")
return " ".join(parts[:-1]), parts[-1]Tip ipucu parametre tiplerini zaten söyler. Docstring'te full_name (str): ... diye tekrar etmeyin; anlamı, yan etkileri, istisnaları, birimleri (seconds, grams) yazın.
Modül docstring'i dosyanın ilk satırıdır:
"""Sipariş durum geçişleri ve iptal kuralları.
Bu paket ödeme altyapısına bağlanmaz; yalnızca alan kurallarını tutar.
"""# ❌
# users: kullanıcı listesi
users = []
# ✅
users: list[User] = []İstisna, kontrol akışının acil çıkış kapısıdır. Temiz kod beklenen, adlandırılmış hataları yakalar; gerisini yutmaz. Zen: Errors should never pass silently. Unless explicitly silenced.
# ❌ Felaket
try:
user = db.get_user(user_id)
user.do_something()
except:
passÜretimde "hiçbir şey olmadı" gibi görünür. Disk dolmuştur, ağ kopmuştur, user None gelmiştir — hepsi aynı karanlığa gider.
# ✅ Dar, loglu, zincirli
try:
user = db.get_user(user_id)
except DatabaseError as exc:
logger.exception("kullanıcı okunamadı", extra={"user_id": user_id})
raise UserStoreUnavailable(user_id) from exc
user.activate()do_something artık try içinde değildir. Aktivasyon hatası, veritabanı hatasıyla karışmaz.
Python geleneği EAFP'dir (Easier to Ask Forgiveness than Permission): dene, KeyError/FileNotFoundError yakala. LBYL (Look Before You Leap) yarış durumuna açıktır: if path.exists() ile open() arasında dosya silinebilir.
# LBYL — TOCTOU riski
if config_path.exists():
text = config_path.read_text()
# EAFP
try:
text = config_path.read_text()
except FileNotFoundError:
text = DEFAULT_CONFIGLBYL, maliyetli veya yan etkili bir çağrıdan kaçınmak için hâlâ doğrudur (ör. ağı denemeden önce boş liste kontrolü).
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise InvalidPayload(str(exc)) from exc
else:
return normalize(payload)
finally:
metrics.increment("payload.parse")else, istisna olmadığında çalışır. Başarı yolunu except ile aynı girinti seviyesinde tutar; try'ı şişirmez.
finally her durumda çalışır. Kilidi, sayacı, tamponu orada bırakırsınız. Dosya ve soket için with tercih edin.
class DomainError(Exception):
"""Alan kuralı ihlali. HTTP katmanı bunu 4xx'e map'leyebilir."""
class InvalidUserInput(DomainError):
pass
class UserNotFound(DomainError):
def __init__(self, user_id: int) -> None:
super().__init__(f"kullanıcı bulunamadı: {user_id}")
self.user_id = user_id
def parse_payload(data: object) -> dict:
if not isinstance(data, dict):
raise InvalidUserInput("gövde bir nesne olmalı")
return dataAlana özgü hatalar, except DomainError ile kenarda yakalanır. ValueError/TypeError standart kütüphane ve küçük yardımcılar için hâlâ doğrudur; her satıra özel sınıf yazmayın.
Sık, beklenen, döngü içi bir durumu istisna ile modellemek pahalı ve gürültülüdür. "Kullanıcı yok" bir API'de istisna olabilir; "satır bulunamadı" bir iç döngüde None veya boş liste olabilir. Ölçüt: istisnai mi, yoksa sıradan mı?
Zen "açıkça susturulmadıkça" der. Dar ve belgelenmiş susturma kabul edilir:
from contextlib import suppress
with suppress(FileNotFoundError):
cache_path.unlink()Bu, "yoksa sorun değil" sözleşmesidir. except Exception: pass değildir.
Tip ipucu (type hint) yorumlayıcıyı değiştirmez; okuyan kişiyi, düzenleyiciyi ve denetleyiciyi değiştirir. Büyük projede "bu None olabilir mi?" sorusunu kod incelemesinde değil, pyright çıktısında sorun.
def add(a: int, b: int) -> int:
return a + bModül sınırını geçen her fonksiyon parametre ve dönüş tipi taşımalıdır. Dosya içi iki satırlık yardımcıda çıkarım yeterlidir; ama emin değilseniz yazın.
# 3.9 ve öncesi
from typing import List, Optional, Dict, Union
def load_names(path: str) -> Optional[List[str]]:
...
# 3.10+
def load_names(path: str) -> list[str] | None:
...Optional[X] ile X | None aynıdır. Rehber X | None kullanır; daha az import, daha az gürültü.
def paginate(
rows: list[User],
*,
offset: int = 0,
limit: int = 20,
) -> tuple[list[User], int]:
return rows[offset : offset + limit], len(rows)Any denetimi kapatır. Kütüphane sınırında, gerçekten dinamik bir noktada, veya göç sırasında geçici olarak kullanılır. Yeni kodda object, Unknown (pyright) veya somut bir Protocol deneyin.
# ❌ Her şeyi yutar
def dump(data: Any) -> str:
return json.dumps(data)
# ✅ JSON'un gerçekten kabul ettiği şekil
def dump(data: Mapping[str, object]) -> str:
return json.dumps(data)from typing import Literal, TypeAlias, TypedDict
UserId: TypeAlias = int
OrderStatus = Literal["draft", "paid", "cancelled"]
class RawUser(TypedDict):
id: int
email: str
is_active: boolJSON sınırında TypedDict işe yarar. Alan modeli büyüyünce dataclass veya Pydantic modeline geçin; TypedDict doğrulama yapmaz.
from typing import Protocol
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None: ...
def notify_welcome(sender: EmailSender, email: str) -> None:
sender.send(email, "Hoş geldiniz", "Hesabınız açıldı.")SmtpEmailSender ve testteki RecordingEmailSender ortak bir taban sınıfa ihtiyaç duymaz. Ördek tipleme, tiplerle belgelenir. ABC'ye göre daha gevşek, Callable çorbasına göre daha okunur.
Bunlar borçtur. Nedenini yorumlayın ve mümkünse kaldırın. cast çalışma anını değiştirmez; denetleyiciye yalan söyler.
Yeni projede public API için ipucu zorunlu, CI'da temel (veya kademeli sıkı) kip önerilir.
Sihirli sayı, anlamı belirsiz çıplak sabittir. Okuyan kişi "0.9 ne?" diye duraksar.
def apply_discount(price: Decimal) -> Decimal:
return price * Decimal("0.9") # %10 mu, özel kampanya mı, yuvarlama mı?DISCOUNT_RATE = 0.9 biraz daha iyidir ama isim hâlâ "neden 0.9"u söylemez. %10 indirim, çarpan 0.9 değil, orandır.
DEFAULT_DISCOUNT_RATE = Decimal("0.10")
def apply_discount(
price: Decimal,
rate: Decimal = DEFAULT_DISCOUNT_RATE,
) -> Decimal:
if not 0 <= rate <= 1:
raise ValueError("oran 0 ile 1 arasında olmalı")
return price * (1 - rate)İstisnalar: 0, 1, -1 gibi matematiksel kimlikler; dilin kendi range(n)'i. "Sayfa boyutu 20" sihirli değildir, iş kuralıdır — isimlendirin.
Sınıf gövdesindeki çıplak oran da aynı kurala uyar. return self.amount * 0.18 hem sihirdir hem de oranı faturaya bağlamaz:
VAT_RATE_DEFAULT = Decimal("0.20")
@dataclass(frozen=True)
class Invoice:
amount: Decimal
vat_rate: Decimal = VAT_RATE_DEFAULT
def tax(self) -> Decimal:
return self.amount * self.vat_rate# ❌
if order.status == "p":
ship(order)
# ✅
class OrderStatus(StrEnum):
DRAFT = "draft"
PAID = "paid"
SHIPPED = "shipped"
CANCELLED = "cancelled"
if order.status is OrderStatus.PAID:
ship(order)StrEnum (3.11+) hem okunur hem serileştirilebilir. 3.10'da class OrderStatus(str, Enum): aynı işi görür. Karşılaştırmada is üyeler için güvenlidir; değerle geliyorsanız OrderStatus(raw) ile dönüştürün.
| İhtiyaç | Yapı |
|---|---|
| Sıralı, yinelenen, indeksli | list |
| Sabit kayıt, sözlük anahtarı | tuple |
| Üyelik, tekillik | set / frozenset |
| Anahtar → değer | dict |
| Uçlardan ekle/çıkar | deque |
| Sayım | Counter |
| Eksik anahtarda fabrika | defaultdict |
| Küçük değişmez kayıt | dataclass(frozen=True) / NamedTuple |
# ❌ Üyelik için liste: O(n)
if user_id in [1, 2, 3, 4, 5]:
...
# ✅
STAFF_IDS = frozenset({1, 2, 3, 4, 5})
if user_id in STAFF_IDS:
...Alan nesnesini dict olarak gezdirmek primitive obsession'dır. user["emial"] yazım hatası çalışma anında patlar; user.email hem tamamlanır hem denetlenir.
# ❌ Klasik tuzak: varsayılan liste süreç boyunca paylaşılır
def add_item(item: str, bucket: list[str] = []) -> list[str]:
bucket.append(item)
return bucket
# ✅
def add_item(item: str, bucket: list[str] | None = None) -> list[str]:
if bucket is None:
bucket = []
bucket.append(item)
return bucketAynı tuzak dict ve özel nesneler için de geçerlidir.
Fonksiyon bir "çift" döndürüyorsa tuple[str, str] kullanın. Çağıran kişi paketini açar. Homojen, uzayan bir dizi list'tir. tuple "bu alanlar sabit" mesajı verir.
Pythonic kod, dilin yerleşik biçimlerini okunabilirliği artırdığı yerde kullanır. Amaç "daha az satır" değil, "daha az sürpriz"dir. Java'dan gelen sınıf töreni de, dört katmanlı liste kavrayışı da Pythonic değildir.
# Kabul edilebilir, ama niyeti gizler
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n * n)
# Pythonic
squares = [n * n for n in numbers]Süzme de sığ olmalıdır:
adults = [user for user in users if user.age >= 18]İki for + if + dönüşüm birleşince kavrayış bir satırlık bulmaca olur. O zaman döngüye dönün. Üreteç kullanın; dev listeyi belleğe zorlamayın:
total = sum(n * n for n in numbers if n % 2 == 0)Sözlük ve küme kavrayışı aynı kurala uyar:
email_by_id = {user.id: user.email for user in users}
active_ids = {user.id for user in users if user.is_active}for index, item in enumerate(items, start=1):
print(f"{index}. {item}")
for name, score in zip(names, scores, strict=True):
print(f"{name}: {score}")strict=True (3.10+) uzunluk uyuşmazlığını yutar. Sessizce kesilen zip gece yarısı bug'udur.
İndeks gerektiğinde range(len(xs)) yazmayın; ya enumerate ya da doğrudan öğe üzerinde dönün.
first, *middle, last = items
lat, lon = coordinates
user_id, email = rowitem[0], item[1] zinciri, kaydın şeklini gizler.
with path.open(encoding="utf-8") as handle:
data = handle.read()Dosya, kilit, bağlantı, işlem: hepsi with. Ayrıntı bölüm 11.
label = "aktif" if user.is_active else "pasif"İç içe üçlü ifade yazmayın. X if A else Y if B else Z bir if/elif hak eder.
if (error := validate(payload)) is not None:
raise InvalidUserInput(error)Her atamayı := yapmak okunabilirliği bozar. Koşulda hemen kullanacağınız bir değeri tekrar hesaplamamak için vardır.
name = "Ada"
print(f"Merhaba, {name}!")
print(f"{total:.2f} {currency}")% ve .format() eski kodda kalabilir. Yeni kodda f-string varsayılandır. Log çağrısında kullanıcı metnini format dizgesi olarak geçmeyin (logger.info(user_text)); %s veya extra= kullanın (bölüm 14).
has_admin = any(user.is_admin for user in users)
all_verified = all(user.has_verified_email for user in users)Boş dizide all([]) True, any([]) False'tur. Bu matematiksel olarak doğrudur; iş kuralınız boş listeyi ayrı ele almalıysa önce onu kontrol edin.
from pathlib import Path
root = Path(__file__).resolve().parent
config = root / "config" / "app.toml"
text = config.read_text(encoding="utf-8")os.path.join yeni kodda varsayılan olmamalıdır. Path nesnesini fonksiyona str diye değil Path diye geçin.
if needle in haystack:
...
language = payload.get("language", "tr")dict[key] yalnızca anahtarın zorunlu olduğunu söylüyorsanız kullanın; yokluğu KeyError ile patlamalıdır.
sum, min, max, sorted, reversed, itertools, collections. El ile yazılmış bir for döngüsü çoğu zaman bir yerleşiğin yeniden keşfidir.
# ❌
total = 0
for n in numbers:
total += n
# ✅
total = sum(numbers)# ❌ Zekice, okunmaz
result = {
k: [x for x in vs if x.ok]
for k, vs in ((g, [f(i) for i in items if p(i)]) for g in groups)
if vs
}Bunu üç isimli adıma bölmek Pythonic'dir: sparse is better than dense.
from datetime import datetime, timezone
# ❌
import time
now = time.time() # anlamsız float, test etmesi zor
then = datetime.now() # yorumlayıcının yerel saati; sunucuda sürpriz
# ✅ 3.11+ için `from datetime import UTC` ve `datetime.now(UTC)` aynıdır
now = datetime.now(timezone.utc)datetime.utcnow() 3.12'de deprecated'tır; datetime.now(timezone.utc) kullanın. Düz dizgi ("2026-08-22") yalnızca sınırda (JSON, CSV) dursun; içeride date / datetime taşıyın. İki zamanı karşılaştırmadan önce her ikisinin de tzinfo taşıdığını doğrulayın — naive ile aware toplamak TypeError fırlatır, bu bir lütuftur; sessizce yerel saate kaymak daha kötüdür.
Açılan her şey kapanmalıdır: dosya, soket, oturum, kilit, geçici dizin, işlem. close()'u try/finally ile hatırlamak kırılgandır; with dilin sözleşmesidir.
from pathlib import Path
path = Path("report.txt")
with path.open("w", encoding="utf-8") as handle:
handle.write("ok\n")İstisna olsa da olmasa da dosya kapanır.
from contextlib import contextmanager
from time import perf_counter
@contextmanager
def timed(name: str):
started = perf_counter()
try:
yield
finally:
elapsed = perf_counter() - started
logger.info("süre", extra={"name": name, "seconds": elapsed})
with timed("import_users"):
import_users()Sınıf biçimi (__enter__ / __exit__) durum tutmanız gerektiğinde daha okunur.
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(path.open()) for path in paths]
merge(files)Dinamik sayıda with için ExitStack vardır. İki sabit kaynak için iç içe with veya virgüllü with yeterlidir:
with src.open() as left, dst.open("w") as right:
right.write(left.read())from contextlib import closing
from urllib.request import urlopen
with closing(urlopen(url)) as response:
body = response.read()Modern kodda httpx / requests.Session zaten context manager sunar. Sunmuyorsa closing ile sarmalayın; __del__'e güvenmeyin.
Nesne yönelimli programlama Python'da bir araçtır, varsayılan din değildir. Birçok problem modül + fonksiyon + dataclass ile biter. Sınıf, durum ve o duruma bağlı davranış bir aradayken hak eder.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return "hav"
class Cat(Animal):
def speak(self) -> str:
return "miyav"speak gövdesinde yalnızca pass olan bir taban sınıf sözleşme zorlamaz; unutulan metot çalışma anında, üstelik geç bir anda patlar. ABC + abstractmethod bunu örnekleme anında yakalar. Yine de Animal yalnızca demo için iyidir; gerçek kodda protokol çoğu zaman yeter.
Kalıtımın maliyeti: üst sınıf değişince tüm alt sınıflar titrer (kırılgan taban sınıf). Şablon metot, kanca metot, super() zinciri — bunlar okumayı yo-yo haline getirir.
class Engine:
def start(self) -> None:
logger.info("motor çalıştı")
class Car:
def __init__(self, engine: Engine) -> None:
self._engine = engine
def drive(self) -> None:
self._engine.start()
logger.info("sürüş")Car bir Engine değildir; bir motor kullanır. Testte Engine yerine sahte bir motor geçersiniz.
class Logger(Protocol):
def log(self, message: str) -> None: ...
class Service:
def __init__(self, logger: Logger) -> None:
self._logger = logger
def run(self) -> None:
self._logger.log("Service running")Bağımlılık dışarıdan gelir (dependency injection). Service içinde FileLogger() üretmek, testi ve alternatif göndericiyi kilitler.
# ❌
class Database:
pass
class MyDatabase(Database):
passAlt sınıf bir davranış eklemiyorsa kalıtım bir etiket yalanıdır. İhtiyacınız bir isimse Database yeter; ihtiyacınız bir sözleşme ise Protocol yazın.
S — Single Responsibility. Invoice tutarı ve vergiyi bilir. InvoicePrinter veya InvoiceMailer çıktıyı bilir. İkisini tek sınıfta toplamak "fatura yöneticisi" adlı çöplük üretir.
O — Open/Closed. Yeni bir ödeme kanalı eklemek için if provider == kulesini büyütmek yerine yeni bir strateji ekleyin:
class PaymentProvider(Protocol):
def charge(self, amount: Decimal) -> None: ...
class Checkout:
def __init__(self, provider: PaymentProvider) -> None:
self._provider = provider
def pay(self, amount: Decimal) -> None:
self._provider.charge(amount)L — Liskov Substitution. Alt tip, üst tipin yerine sürprizsiz geçebilmelidir. Square(Rectangle) klasik ihlaldir: kare, dikdörtgenin set_width sözleşmesini bozar. Python'da ördek tipleme ihlali daha sinsidir: aynı metot imzası, farklı istisna.
I — Interface Segregation. "Her şeyi bilen" bir UserManager protokolü yerine UserReader, UserWriter, PasswordResetter gibi dar yüzeyler.
D — Dependency Inversion. Üst seviye (kayıt akışı) alt seviye SMTP ayrıntısına değil, soyut EmailSender'a bağlıdır. Import yönü: alan katmanı altyapıyı import etmez; altyapı alanın protokolünü uygular.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Money:
amount: Decimal
currency: str
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError("tutar negatif olamaz")frozen=True değeri anahtar ve paylaşılabilir yapar. slots=True (3.10+) bellek ve yazım hatası (money.ammount) için faydalıdır.
ABC, zorunlu bir taban uygulaması ve paylaşılan yardımcı metot gerektiğinde durur. Yalnızca "şu metot olsun" diyorsanız Protocol yeter.
@dataclass
class Invoice:
amount: Decimal
vat_rate: Decimal
@property
def tax(self) -> Decimal:
return self.amount * self.vat_rateÖzellik, hesap ucuz ve yan etkisizse uygundur. Veritabanı okuyan @property bir yalandır; metot olsun: load_tax().
Çalışan kod yetmez; kodun nerede durduğu da bir tasarımdır. Modül, dosya sistemindeki SRP'dir.
# ❌
from models import *
from .utils import *
# ✅
from shop.models import Order
from shop.pricing import price_with_vatimport * hem isim çakışması hem de "bu isim nereden geldi?" sorusunu üretir. Göreli import paketin içinde kabul edilir; uygulamayı script gibi çalıştırırken kırılgan olabilir. Paketi paket olarak çalıştırın (python -m shop).
Kamu yüzeyi için __all__ belgelenir, her şeyi dışa açmaz.
# shop/pricing.py
__all__ = ["price_with_vat", "vat_amount"]a → b → a çoğu zaman iki kavramın tek dosyada yaşaması gerektiğini veya üçüncü bir tipler / sözleşmeler modülünün eksik olduğunu söyler. Çözüm importı fonksiyon içine gizlemek değil (geçici yara bandı), sınırları yeniden çizmektir.
myproject/
├── pyproject.toml
├── README.md
├── src/
│ └── shop/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── pricing.py
│ └── services/
│ └── checkout.py
└── tests/
├── conftest.py
└── test_pricing.py
src düzeni, yüklü paketi test etmenizi sağlar; tests'in rastgele proje kökünü import etmesini zorlaştırır.
Küçük bir betik için bu abartıdır. On dosyayı geçen, dağıtılan veya birden fazla kişinin dokunduğu her şey için değildir.
Çerçeve kullanıyorsanız onun dilini bozmayın:
myproject/
├── auth_app/
│ ├── views.py
│ ├── models.py
│ └── urls.py
├── shop/
│ ├── views.py
│ ├── models.py
│ └── urls.py
└── config/
├── settings.py
├── urls.py
└── wsgi.py
Her uygulama bir iş alanıdır. utils uygulaması bir iş alanı değildir; oraya kaçan her şey evsizdir.
# ❌
SMTP_PASSWORD = "hunter2"
DEBUG = True
# ✅ Ortam + şema
# pydantic-settings veya os.environ, tek bir Settings nesnesi
class Settings(BaseSettings):
debug: bool = False
smtp_password: SecretStr
database_url: PostgresDsnGizli değer dosyaya gömülmez. DEBUG'ı üretimde unutmak bir yapılandırma hatasıdır, kod stili değil; ama temiz proje bunu tesadüfe bırakmaz.
Modül import edilince iş yapmamalıdır: ağ çağrısı, input(), dosya yazma, basicConfig. Yan etki main() içindedir.
# shop/__main__.py
def main() -> None:
settings = Settings()
run_app(settings)
if __name__ == "__main__":
main()python -m shop bu kapıdan girer. Üst seviyedeki users = db.fetch_all() hem testi hem import shop'u kırar.
Bağımlılık, Python sürümü, Ruff, pytest, araç ayarları bir dosyada yaşar. 2026'da yeni proje için requirements.txt + setup.py + .flake8 + mypy.ini yığınına gerek yoktur. Göç ediyorsanız kademeli gidin; yeni işi eski yığına eklemeyin.
print bir prototip aracıdır. Üretimde log, seviyeli, bağlamlı ve makineyle taranabilir olmalıdır.
| Seviye | Ne zaman |
|---|---|
| DEBUG | Geliştirme ayrıntısı, üretimde kapalı |
| INFO | Önemli iş olayı: "sipariş oluştu" |
| WARNING | Geçici, beklenen sapma: "yeniden denenecek" |
| ERROR | İş tamamlanamadı |
| CRITICAL | Süreç ayakta kalamayabilir |
logger.info("sipariş oluştu", extra={"order_id": order.id, "user_id": user.id})
logger.exception("ödeme alınamadı", extra={"order_id": order.id})logger.exception yalnızca except bloğunda: yığını ekler. logger.error(..., exc_info=True) eşdeğerdir.
# ❌ Kullanıcı metni format dizgesi olur; %s içerirse logging bozulur veya şaşırır
logger.info(email)
# Kabul edilebilir ama yapısal değil; interpolasyon hemen yapılır
logger.info(f"giriş denemesi email={email}")
# ✅ Dizge sizin, değer ayrı
logger.info("giriş denemesi email=%s", email)
logger.info("giriş denemesi", extra={"email": email})Parola, oturum çerezi, kart numarası, erişim jetonu, TC kimlik, sağlık verisi. extra= ile geçseniz bile süzün. Yapısal log (structlog) + maskeleme, metin birleştirmesinden daha güvenlidir.
CLI aracının kullanıcıya yazdığı çıktı print (veya rich) olabilir. Kütüphane kodu print etmez; loglar veya sessiz kalır. Kütüphanede basicConfig de çağırmayın; yapılandırmayı uygulamaya bırakın.
import logging
logger = logging.getLogger(__name__)__name__ hiyerarşisi (shop.checkout) filtrelemeyi mümkün kılar.
Temiz kod, otomatik test olmadan iddiadır. Test, tasarımı da düzeltir: bağlanamayan bağımlılık, gizli global, 80 satırlık fonksiyon — test yazınca ortaya çıkar.
# ❌ Global + gizli I/O
TAX = 0.2
def total():
items = json.loads(Path("cart.json").read_text())
return sum(i["price"] for i in items) * (1 + TAX)
# ✅ Bağımlılıklar parametre
def cart_total(items: list[Item], vat_rate: Decimal) -> Decimal:
return sum((item.price for item in items), start=Decimal("0")) * (1 + vat_rate)Dosya okuma üst seviyede kalır; kural testte bellekte çalışır.
unittest.TestCase çalışır; yeni kodda pytest daha az tören, daha iyi fixture ve assert mesajı sunar.
# pricing.py
from decimal import Decimal
def apply_discount(price: Decimal, rate: Decimal) -> Decimal:
if price < 0:
raise ValueError("fiyat negatif olamaz")
if not 0 <= rate <= 1:
raise ValueError("oran 0 ile 1 arasında olmalı")
return price * (1 - rate)# test_pricing.py
from decimal import Decimal
import pytest
from shop.pricing import apply_discount
def test_apply_discount_ten_percent() -> None:
assert apply_discount(Decimal("100.00"), Decimal("0.10")) == Decimal("90.00")
def test_apply_discount_rejects_negative_price() -> None:
with pytest.raises(ValueError, match="negatif"):
apply_discount(Decimal("-1"), Decimal("0.10"))Arrange — Act — Assert. Test adı koşulu ve beklentiyi söyler:
test_<birim>_<koşul>_<beklenen>
test_apply_discount_ten_percent
test_withdraw_insufficient_funds_raises
test_1, test_works bir isim değildir.
@pytest.mark.parametrize(
("price", "rate", "expected"),
[
(Decimal("100"), Decimal("0.00"), Decimal("100")),
(Decimal("100"), Decimal("0.10"), Decimal("90")),
(Decimal("100"), Decimal("1.00"), Decimal("0")),
],
)
def test_apply_discount_table(
price: Decimal, rate: Decimal, expected: Decimal
) -> None:
assert apply_discount(price, rate) == expected@pytest.fixture
def sample_user() -> User:
return User(id=1, email="ada@example.com", is_active=True)
def test_activate_already_active_is_idempotent(sample_user: User) -> None:
activate(sample_user)
activate(sample_user)
assert sample_user.is_active is True
def test_export_writes_header(tmp_path: Path) -> None:
target = tmp_path / "out.csv"
export_users([], target)
assert target.read_text(encoding="utf-8").startswith("id,email")Gerçek ev dizinine, gerçek /tmp altına rastgele yazmayın. tmp_path test bitince gider.
def test_register_user_sends_welcome(monkeypatch: pytest.MonkeyPatch) -> None:
sent: list[str] = []
def fake_send(email: str) -> None:
sent.append(email)
monkeypatch.setattr(user_service, "send_welcome_email", fake_send)
register_user({"email": "ada@example.com"})
assert sent == ["ada@example.com"]Daha temizi: EmailSender protokolünü test çiftiyle enjekte etmek. Her şeyi mock'layan test, mock'un kendisini test eder; refaktörde kırılır, üretim hatasını kaçırır.
Kapsam (coverage) bir ışıktır, hedef değil. %100 ve anlamsız test, %70 ve kritik yolların kilitlenmesinden kötüdür. Alan kurallarında yüksek kapsam isteyin; ince sarmalayıcılarda takılmayın.
pip install pytest pytest-cov
pytest
pytest --cov=shop --cov-report=term-missinguv kullanıyorsanız: uv run pytest. CI aynı komutu çalıştırsın; "benim makinemde geçti" bir strateji değildir.
Biçim tartışması en pahalı, en az değer üreten tartışmadır. Bir araç seçin, CI'ya bağlayın, insanı biçim polisi olmaktan çıkarın.
PEP 8 girinti (4 boşluk), isimlendirme, import grupları ve genel tadı tanımlar. Satır uzunluğu önerisi 79'dur; Black/Ruff geleneği 88, birçok ekip 100 kullanır. Önemli olan sayı değil, tek sayıdır.
| İş | Araç |
|---|---|
| Ortam ve bağımlılık | uv (pip + venv + lock) |
| Lint + format + import sırası | Ruff |
| Tip | pyright veya ty (köklü projede mypy) |
| Test | pytest |
| Commit kapısı | pre-commit |
| Yapılandırma | pyproject.toml |
Ruff; Flake8 + onlarca eklenti + isort + Black uyumlu biçimlendirici + pyupgrade işini tek ikili dosyada, çok daha hızlı yapar. Eski rehberdeki Black / isort / Flake8 / Pylint listesi hâlâ çalışır. Yeni projede varsayılanınız Ruff olsun. Pylint derin, yavaş ve gürültülüdür; özel bir ihtiyaç yoksa Ruff yeter.
ruff check .
ruff check --fix .
ruff format .[project]
name = "shop"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = []
[dependency-groups]
dev = ["pytest>=8", "ruff>=0.8", "pre-commit>=4"]
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "RUF"]
[tool.pytest.ini_options]
testpaths = ["tests"]# ❌
def getuser( id ):
return db.get( id )
# ✅
def get_user(user_id: int) -> User:
return db.get(user_id)id yerleşiktir; parametre adı user_id olsun.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatKanca, "unutulan import"ı inceleme yorumu olmaktan çıkarır.
Anti-pattern, kısa vadede iş görür gibi duran, uzun vadede bakımı, testi ve teşhisi bozan yaklaşımdır. Fark edilmezse alışkanlık, alışkanlık teknik borç olur.
try:
risky_operation()
except:
pass# ❌
counter = 0
def increment() -> None:
global counter
counter += 1Gizli bağımlılık, paralel test, "kim değiştirdi?" sorusu.
# ✅
@dataclass
class Counter:
value: int = 0
def increment(self) -> None:
self.value += 1Daha iyisi: sayacı ihtiyacı olan nesnenin alanı yapın; dünya çapında tekil (singleton) ilan etmeyin.
UserManager, AppService, helpers.py, utils.py — her yeni iş buraya düşer. Bölün: isim yapılan işi sınırlasın (password_reset.py, invoicing.py).
Para için float, durum için "p", e-posta için çıplak str ve her yerde aynı doğrulama. Decimal + para birimi, Enum, doğrulanmış Email tipi.
float para için yanlıştır: 0.1 + 0.2.
# ❌
def save_user(user: User, send_email: bool) -> None:
...
# ✅
def save_user(user: User) -> None:
...
def save_user_and_welcome(user: User) -> None:
save_user(user)
send_welcome_email(user.email)import * isim uzayını kirletir. def foo(*args, **kwargs) her şeyi yutan bir imza ise tipi, dokümantasyonu ve çağrı yerini yok eder. Gerçekten iletme katmanındaysanız (def wrap(*args, **kwargs) → inner(*args, **kwargs)) kabul; aksi halde parametreleri yazın.
Kullanıcı girdisini eval etmek, bellek görüntüsünü pickle ile yabancıdan almak — bunlar stil değil, güvenlik deliğidir. Bölüm 20.
def do() -> int:
x = 1
return xYa silin ya da neden bir olduğunu isimle anlatın.
i = 0 # sayaç
# eski_algoritma(i)İsmi düzeltin, ölü kodu silin.
Bir metot durmadan başka nesnenin alanlarını kurcalıyorsa, o davranış diğer nesneye aittir.
# ❌
def invoice_total(invoice: Invoice) -> Decimal:
return invoice.amount + invoice.amount * invoice.vat_rate
# ✅
class Invoice:
def total(self) -> Decimal:
return self.amount + self.taxTek bir iş kuralı değişince on dosyayı aynı anda yamalıyorsanız, bilgi dağılmıştır. DRY'ye dönün; ama AHA'yı unutmayın.
Okunabilirlik, kısa yazmaktan önemlidir. Üç karakter kazanan bir isim, üç ay kaybettiren bir hatadır.
Anti-pattern'ler code review ve bu listenin ekipçe sahiplenilmesiyle geriler. Belgelenmeyen kural, kural değildir.
Refactoring, dış davranışı değiştirmeden iç yapıyı iyileştirmektir. Yeni özellik değildir. İkisi aynı commit'te karışırsa "ne bozuldu?" sorusunun cevabı kaybolur.
Ne zaman değil: teslimden bir saat önce, test yokken, "madem açtık her şeyi yeniden yazalım" anında.
Martin Fowler'ın katalogundaki birkaç dönüşüm günlük işin yüzde doksanını karşılar:
| Dönüşüm | Ne zaman |
|---|---|
| Rename | İsim yalan söylüyor veya eksik |
| Extract Function | Bloğu yüksek sesle "ve sonra" diye okuyorsunuz |
| Extract Variable | İfade kendi cümlesini hak ediyor |
| Replace Magic Number | Çıplak 0.20, "paid", 86400 |
| Introduce Parameter Object | 4+ parametre veya sürekli birlikte gezinen değerler |
| Replace Conditional with Polymorphism / dict | Büyüyen if provider == kulesi |
| Move Function | Davranış yanlış evde (feature envy) |
| Encapsulate Collection | Dışarıya çıplak liste sızıyor, herkes mutasyona uğratıyor |
# ❌
def export_orders(orders: list[Order], path: Path) -> None:
lines = ["id,total,status"]
for order in orders:
if order.status is OrderStatus.CANCELLED:
continue
total = order.amount + order.amount * order.vat_rate
lines.append(f"{order.id},{total},{order.status.value}")
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
# ✅
def _csv_row(order: Order) -> str:
return f"{order.id},{order.total()},{order.status.value}"
def export_orders(orders: list[Order], path: Path) -> None:
active = [order for order in orders if order.status is not OrderStatus.CANCELLED]
lines = ["id,total,status", *(_csv_row(order) for order in active)]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")Vergi hesabı Invoice.total / Order.total içine taşındı (feature envy çözüldü). CSV biçimi ayrı. Dosya yazma ayrı. Her parça tek başına test edilir.
Kokuyu ezberlemek değil, harekete bağlamak işe yarar:
Koku bir suçlama değildir. "Burada bir karar gecikmiş" notudur.
Kod incelemesi (code review) bir kapı bekçiliği değil, ortak sahiplik ritüelidir. Temiz kod, tek kişinin zevki olarak yaşamaz; ekibin konuşabildiği bir sözlük olarak yaşar.
Sıra önemlidir. Biçim yorumu otomatikleştirilmişse insan zamanı karara gider.
Biçim, import sırası, tırnak stili — Ruff geçtiyse yoruma konu değildir. "Şöyle daha Pythonic olur" ancak okunabilirlik gerçekten artıyorsa yazılır.
blocking: `except Exception` burada bağlantı koparma ile şema hatasını aynı yola sokuyor.
Öneri: `DatabaseError`'ı yakalayıp zincirleyelim; geri kalanı yükselsin.
Yazar göndermeden önce 23. Kontrol Listesi'ni kendi kendine uygulayabilir. İnceleyen aynı listenin kısa halini kullanır: isimler, istisnalar, test, sır, log, kapsam.
Temiz kod güvenli kod değildir; ama kirli kod güvenliği incelemeyi imkânsızlaştırır. Bu bölüm saldırı tarifi değildir. Üretim Python'unda sık kırılan hijyen kurallarıdır.
# ❌
API_KEY = "sk-live-...."
SMTP_PASSWORD = "hunter2"
# ✅
# ortam değişkeni veya gizli kasa; Settings nesnesi
api_key = settings.api_key.env commit edilmez. Örnek dosya .env.example yalnızca anahtar adlarını taşır. Log, hata sayfası, örnek fixture — hiçbiri canlı sır içermez. Düz metin sır bir kez git geçmişine girdiyse silmek yetmez; anahtarı döndürün.
Kullanıcı metnini SQL, kabuk komutu veya LDAP süzgecine dizgi birleştirerek koymayın.
# ❌
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
# ✅ parametreli sorgu
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))ORM kullanıyorsanız ham SQL'e düştüğünüz her yerde aynı kural geçerlidir. Kabuk için subprocess listesi kullanın, shell=True + kullanıcı dizgesi kullanmayın.
# ❌
subprocess.run(f"convert {user_filename} out.png", shell=True)
# ✅
subprocess.run(["convert", user_filename, "out.png"], check=True)Dosya adı bile ../ içerebilir; yolu 20.3 ile sınırlayın.
Kullanıcının verdiği dosya adı doğrudan Path("/var/data") / filename olmamalıdır. resolve() sonrası kökün altında kaldığını doğrulayın. Yüklenen dosyanın uzantısına güvenmeyin; içerik türünü sunucu tarafta sınırlayın.
Kilit dosyası (uv.lock, poetry.lock) commit edilir. Sürüm aralığını başıboş bırakmak, yarın sabah kırılan bir alt bağımlılıktır. Bilinen açıklar için pip-audit / uv audit benzeri bir tarama CI'da durmalıdır. "Star sayısı yüksek" bir güvenlik incelemesi değildir.
Temiz bir get_user(user_id) yetkiyi unutursa, güzel isimli bir deliktir. Sorgu istenilen kaydı değil, bu aktörün görebileceği kaydı döndürmelidir. Log ve destek dökümünde maskeleme (bölüm 14) aynı disiplindir.
Güvenlik bir bölümün işi bitmez. Tehdit modeliniz (kim, neyi, nereden) yoksa bu madde listesi bir başlangıçtır, kapanış değil.
async def kirli kodu hızlandırmaz; kirli kodu zaman içinde dağıtır. Temizlik kuralları senkron kodla aynıdır: isim, tek sorumluluk, görünür yan etki. Ek olarak iptal, zaman aşımı ve "kim bekliyor?" sorusu gelir.
# ❌ Olay döngüsünü bloklar
async def fetch_profile(user_id: int) -> Profile:
raw = Path("cache.json").read_text() # senkron disk
time.sleep(0.1) # senkron uyku
return parse(raw)
# ✅
async def fetch_profile(user_id: int) -> Profile:
async with aiofiles.open("cache.json") as handle:
raw = await handle.read()
await asyncio.sleep(0.1)
return parse(raw)Bloklayan çağrı zorunluysa asyncio.to_thread ile işaretleyin; saklamayın. Bir fonksiyon async ise çağıran await eder — içinde time.sleep görmek bir yalandır.
Ekipçe bir kural seçin ve sapmayın:
Karışık paket en kötüsüdür: get_user senkron, get_user2 asenkron, fetch_user hangisi belirsiz.
async def load_rates() -> Rates:
async with asyncio.timeout(2.5):
return await client.get_rates()Süresiz await üretimde bir goroutine sızıntısı değil, istek sızıntısıdır. asyncio.timeout (3.11+) veya asyncio.wait_for kullanın. İptal (CancelledError) yutulmamalıdır; kaynak try/finally veya with ile kapanmalıdır.
# ❌ Her hatayı, belki de iptali, "yok"a çevirir
try:
await long_task()
except Exception:
return None
# ✅ 3.9+ `CancelledError` `BaseException`'dır; yine de dar yakalayın
try:
await long_task()
except TimeoutError:
logger.warning("oran isteği zaman aşımı")
raiseresults = await asyncio.gather(fetch_a(), fetch_b(), return_exceptions=True)return_exceptions=True hataları yutmaz; onları değer yapar. Her sonucu isinstance(..., Exception) ile ayırmadan "hepsi başarılı" varsaymayın. Varsayılan gather ilk hatada diğerlerini iptal eder — bu çoğu zaman istediğiniz davranıştır; değilse belgeleyin.
# ❌ Kütüphane kodu
def get_user(user_id: int) -> User:
return asyncio.run(fetch_user(user_id))asyncio.run bir sürecin giriş noktasındadır (main). Kütüphane, çağıranın döngüsüne await ile katılır. Hem senkron hem asenkron API sunacaksanız iki açık fonksiyon yazın; birinin içinde diğerini gizlice run etmeyin.
async tek iş parçacıklıdır ama beklemeler arasında başka görevler araya girer. cache[key] = await fetch() yarışına açıktır: iki görev aynı anahtarı birlikte doldurur. Kilidi (asyncio.Lock) veya tek uçuş (cache in-flight) açık tutun. "Zaten senkron değil" diye global dict mutasyonu güvenli değildir.
Hızlı kod, ölçülemeyen bir iddia; okunur kod, varsayılan hedeftir. Zen: practicality beats purity — ama pratiklik, tahmin değil ölçümdür.
"Liste kavrayışı for'dan hızlıdır" çoğu iş yükünde görünmez. "Her istekte 400 ms'lik HTTP'yi döngüde sıralı yapmak" görünür. Önce ikincisi.
# Üyelik
allowed = frozenset(allowed_ids)
if user_id in allowed: # liste yerine
...
# Üreteç: dev ara liste yok
total = sum(order.total() for order in orders if order.is_billable)
# I/O biriktirme
# 3.11+: TaskGroup. 3.10'da asyncio.gather(*tasks)
async with asyncio.TaskGroup() as group:
for url in urls:
group.create_task(fetch(url))for + append yerine kavrayış hem daha okunur hem genellikle yeterince hızlıdır. Kazanç yan etkidir, mikrosaniye değil.
Sayısal çekirdek, sıkı bir döngü, bir ayrıştırıcı: burada slot, NumPy, Cython veya satır içi genişletme meşrudur. O bloğu küçük tutun, ölçümü yoruma yazın, dış API'yi temiz bırakın.
def checksum(data: bytes) -> int:
# py-spy: %40 zaman burada; C-uzantısı ayrı iş. Döngü kasıtlı.
total = 0
for byte in data:
total = (total + byte) & 0xFFFFFFFF
return totalOkunabilirlik, ekibin hızıdır. Performans, kullanıcının zamanıdır. İkisini de tahminle harcamayın.
PR göndermeden veya bir dosyayı "bitirdim" demeden önce. Hepsi her seferinde uygulanmaz; bilinçli atlama uygulanır.
PEP metinleri resmi olarak İngilizcedir; çeviri varsa yardımcı, asıl sözleşme İngilizce sürümdür.
| Terim | Bu rehberde |
|---|---|
| Temiz kod | Bir sonraki okuyanın güvenle değiştirebildiği kod |
| Teknik borç | Erken teslim için alınan, faiz işleten tasarım kısa yolu |
| SRP | Bir birimin tek değişme gerekçesi |
| DRY | Bilginin tek yaşama noktası; metin kopyası değil |
| AHA | Acele soyutlamama; benzer ≠ aynı |
| EAFP | İzni sormak yerine dene, belirli istisnayı yakala |
| Yan etki | Fonksiyonun dönüş değeri dışında dünyayı değiştirmesi |
| Protokol | Kalıtımsız, ördek tipli sözleşme |
| Guard clause | Mutlu yoldan önce erken return / raise |
| Karakterizasyon testi | Mevcut davranışı, "doğru" olup olmadığına bakmadan kilitleyen test |
Bu rehber yaşayan bir belgedir. Hata, eksik örnek veya katılmadığınız bir kural için katkı rehberine bakın. Tartışılabilir her kural, gerekçesiyle birlikte daha temiz hale gelir.
MIT Lisansı — LICENSE
| Back | FazBrowse Home | New Git URL |