"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
- Sırayla okumak en iyisidir: isimlendirme ve fonksiyonlar, sonraki her şeyin zeminidir.
- Zaten orta seviyedeyseniz içindekilerden atlayabilirsiniz; 23. Kontrol Listesi günlük iş için yeterlidir.
- Örneklerdeki
db,logger,Usergibi isimler bağlamsal iskelettir; kopyalayıp çalıştırmak için değil, kararı görmek içindir. - ❌ kötü örnekler kasıtlı olarak bozuktur. ✅ iyi örnekler tek doğru yol değildir; o problem için daha temiz bir yoldur.
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.
- 1. Giriş ve Temel Felsefe
- 2. İsimlendirme
- 3. Fonksiyonlar ve Tek Sorumluluk
- 4. Kontrol Akışı ve Karmaşıklık
- 5. DRY, KISS, YAGNI ve AHA
- 6. Yorumlar, Docstring ve Anlatım
- 7. İstisna Yönetimi
- 8. Tip İpuçları
- 9. Sihirli Değerler, Enum ve Veri Yapıları
- 10. Pythonic Kod Yazımı
- 11. Kaynak Yönetimi ve Context Manager
- 12. OOP, SOLID ve Kompozisyon
- 13. Modülerlik ve Proje Yapısı
- 14. Loglama
- 15. Test Edilebilir Kod ve Test Pratikleri
- 16. Biçimlendirme, Linter ve Araç Zinciri
- 17. Kötü Pratikler (Anti-Pattern'ler)
- 18. Refactoring
- 19. Kod İncelemesi
- 20. Güvenlik Temelleri
- 21. Asenkron Kodda Temizlik
- 22. Performans ve Okunabilirlik
- 23. Kontrol Listesi
- 24. Ek Kaynaklar
- Sözlük
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.
- Okuma, yazmadan fazladır. Bir satırı bir kez yazar, onlarca kez okursunuz. Okuma maliyetini düşürmek, yazma maliyetini düşürmekten daha çok iş kazandırır.
- Takım, sizin bağlamınıza sahip değildir. Değişkenin
dolmasının nedenini stand-up'ta anlattıysanız bile, o bağlam commit'e girmez. - Hata, belirsizlikte ürer. İsmi yalan söyleyen fonksiyon, yanlış yakalanan istisna, kopyalanmış ve yarısı güncellenmiş bir blok — bunlar "zor bug"ların sıradan kaynaklarıdır.
- Değişiklik ucuz olmalıdır. Temiz kod, yeni özelliği eklemeyi kolaylaştırır; kirli kod, yeni özelliği cesaret etmeyi zorlaştırır.
- Test, tasarımın aynasıdır. Test yazılamayan kod genellikle fazla bağlı, fazla yan etkili veya fazla büyüktür.
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.
- İzci kuralı (Boy Scout Rule): Kamp yerini, bulduğunuzdan biraz daha temiz bırakın. Bir dosyayı açtıysanız, en azından o fonksiyondaki yanıltıcı ismi veya sihirli sayıyı düzeltin.
- Kırık pencere: Kirli bir köşe, yanına ikinci kiri çeker. "Zaten burası dağınık" gerekçesi, dağınıklığı evren yasası haline getirir.
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.
- İsim, neyi temsil ettiğini söylemeli; nasıl hesaplandığını değil.
- Okuyan kişi, tanımı görmeden değişkeni bir cümlede kullanabilmeli.
- Kısa ama boş (
d,tmp2) veya uzun ama belirsiz (data_info_value_result) isimlerden kaçının. - Kısaltmayı yalnızca ekipçe paylaşılan bir sözlükte tutun (
id,url,httpkabul;usrMgr,tmpAccdeğil). - Yerleşik isimleri gölgelemeyin:
id,list,type,str,hash,input,file. - Alan dilini kullanın. Muhasebe kodunda
fatura, finans kodundaledger— rastgelemanager2değil.
| 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.
- Fonksiyon bir fiildir:
get_user,calculate_tax,send_invoice. - Sınıf bir isimdir:
User,InvoiceRepository,SmtpEmailSender. - Yan etki varsa isimde gizlemeyin.
get_userveritabanına yazıyorsa isim yalandır;get_or_create_userveyafetch_user+ ayrıcreate_userdeyin. handle,process,do,manage,performancak gerçekten bir orkestrasyon noktasındaysa kabul edilir; aksi halde işi söyleyin.
# ❌
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.
- İsim fiil temelli olsun; gövde ismi doğrulasın.
- Parametre sayısı mümkünse 0–3 arasındadır. Dördüncü parametre çoğu zaman bir nesnenin eksik doğduğunun işaretidir.
- Boolean bayrak parametresi (
send_email: bool) fonksiyonu iki fonksiyona böler. İki fonksiyon yazın veya strateji geçin. - Bir fonksiyon hem soru sorup hem dünyayı değiştiriyorsa okuyan her çağrıda irkilir. Mümkünse Komut–Sorgu Ayrımı (Command–Query Separation): sorgular yan etkisiz, komutlar değeri değil durumu değiştirir.
- Gövdeyi yüksek sesle okuduğunuzda "ve", "sonra da", "bir de" geliyorsa fonksiyon büyümüştür.
- İç içe üç seviyeden fazla girinti, bölünme çağrısıdır.
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.
- Önce isim ve yapı; sonra yorum.
- Ne yaptığını değil, neden yaptığını yazın. "Ne" zaten kodda durur.
- Yorum, kodla birlikte yaşlanır. Yalan söyleyen yorum, yorumsuz koddan tehlikelidir.
- Bağırmayın, şaka yapmayın, kişiyi hedef almayın.
- Commented-out kod commit etmeyin. Git o işi daha iyi yapar.
# ❌ 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.
- Yalnızca beklediğiniz ve yönetebileceğiniz tipleri yakalayın.
- Çıplak
except:yazmayın.KeyboardInterruptveSystemExitdahil her şeyi yutar. except Exception:bile geniştir. Log + yeniden fırlat dışında nadiren doğrudur.trybloğunu mümkün olduğunca dar tutun. Aksi halde hangi satırın patladığını bilemezsiniz.- Mesaj, hangi sözleşmenin bozulduğunu söylesin.
- Zinciri koparmayın:
raise NewError(...) from exc. - Kaynak temizliği için
finallyveya daha iyisiwith.
# ❌ 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.
- pyright (Pylance'ın motoru) veya Astral'ın ty'si: hızlı, standartlara yakın.
- mypy --strict: köklü ekosistem, yavaş ama olgun.
- İkisini birden "hakem" yapmayın; ekip birini seçsin, CI o olsun.
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.
- Her sınıfın tek bir değişme gerekçesi olsun (SRP).
- Kalıtım, gerçek bir "is-a" + aynı yaşam döngüsü varsa kullanın.
- Tereddütte kompozisyon seçin.
- Sınıflar birbirine gevşek bağlansın; somut SMTP sınıfını her yere gömmeyin,
EmailSenderprotokolü geçin. - Boş
Base*ve tek satırlık alt sınıf yazmayın.
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.
- Her modülün bir cümlelik işi olsun.
- Döngüsel import, sınırların yanlış çizildiğinin semptomudur.
- Yeniden kullanılabilirlik, kopyala-yapıştırın değil, net bir kamu API'sinin ürünüdür.
- Test, import edebildiğiniz şeyi test eder. Tanrı modül (
utils.py+ 1400 satır) test edilemez; korkulur.
# ❌
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.
- Birim, tek başına kurulabilmelidir.
- Saf fonksiyon (aynı girdi → aynı çıktı, yan etkisiz) en ucuz birimdir.
- Test, kodun sözleşmesini kilitler; her satırını değil.
- Manuel "çalıştır bak" bir güvenlik ağı değildir.
- Test hızlı, tekrarlanabilir, bağımsız olsun. Sıra bağımlı test, flaky testin akrabasıdır.
# ❌ 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.
- Ruff/biçimlendiricinin işi (virgül stili).
- Üçüncü parti kütüphanenin kendi sözleşmesi (Django'nun
QuerySet.filterini yeniden test etmeyin). - Bire bir uygulama detayı ("bu özel metot
_cache'e yazar"). Davranışı kilitleyin.
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"]- Kaydetmede format, kaydetmede değilse pre-commit.
- CI:
ruff check,ruff format --check, tip denetimi,pytest. - Biçim ile davranış aynı PR'da karışmasın. Önce format commit'i, sonra iş commit'i — veya formatı herkes için otomatik yapın ki diff kirlenmesin.
# ❌
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.
- Bir dosyaya üçüncü kez korkarak giriyorsanız.
- Test yazamıyorsanız.
- Kopyalanmış iş kuralı yakaladıysanız.
- İsim yalan söylüyorsa.
- PR'da "anlamadım" yorumu düştüyse.
Ne zaman değil: teslimden bir saat önce, test yokken, "madem açtık her şeyi yeniden yazalım" anında.
- Davranışı kilitleyen bir test yoksa önce onu yazın. Karakterizasyon testi (bugünkü davranışı olduğu gibi kilitleyen test) bile, "güzelleştirirken bozdum" felaketini önler.
- Tek bir dönüşüm uygulayın. Karıştırmayın.
- Testleri çalıştırın.
- Commit edin. Küçük commit, geri alınabilir commit'tir.
- Sonraki dönüşüme geçin.
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.
- Test yokken "büyük temizlik".
- Refactor + özellik + biçim + bağımlılık yükseltmesi tek PR.
- "Madem buradayız framework değiştirelim."
- Davranışı sessizce değiştirmek (ör.
float→Decimalyuvarlama farkı). Bu refactor değil, iş değişikliğidir; testte görünür kılın. - Kullanılmayan soyutlama eklemek (YAGNI). Refactor sadeleştirir; "ileride esnek olur" diye katman eklemez.
Kokuyu ezberlemek değil, harekete bağlamak işe yarar:
- Uzun fonksiyon → Extract Function, guard clause
- Uzun parametre listesi → Parameter Object
- Yinelenen blok → Extract + AHA kontrolü
- Switch/if kulesi → tablo veya strateji
- Reddedilen miras → kompozisyon
- Açıklayıcı yorum → Rename
- Data class + dışarıda duran ilgili fonksiyonlar → Move Function
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.
- Doğruluk: Bu değişiklik iddia ettiği işi yapıyor mu? Kenar durum? Yarış?
None? - Sözleşme: İsim, tip, istisna, log — okuyan kişi çağrı yerinde şaşıyor mu?
- Tasarım: Yeni bir kavram mı doğdu, yoksa mevcut bir kavram mı şişti? Yanlış katmana mı kaçtı?
- Test: Davranış kilitli mi, yoksa yalnızca mutlu yol mu var?
- Güvenlik ve gizlilik: Gizli değer, enjeksiyon, aşırı log, yetki.
- Operasyon: Bu hata üretimde nasıl görünecek? Metrik, log, geri dönüş.
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.
- Kişiyi değil, kodu konuşun. "Bu isim
userileaccountı karıştırıyor" — "sen yine saçmalamışsın" değil. - Sorun + gerekçe + varsa alternatif. "Neden?" cevapsız bırakılan nitpick, gürültüdür.
- Zorunlu / öneri / merak ayrımını işaretleyin:
nit:,suggestion:,blocking:. - Övgü de incelemedir. İyi bir koruma cümlesi veya net bir test adı, tekrar edilsin diye işaretlenir.
- Tartışma 5-6 yorumu geçiyorsa asenkron ipliği bırakın; 15 dakikalık bir çağrı + sonucu PR'a yazmak daha ucuzdur.
blocking: `except Exception` burada bağlantı koparma ile şema hatasını aynı yola sokuyor.
Öneri: `DatabaseError`'ı yakalayıp zincirleyelim; geri kalanı yükselsin.
- Küçük PR. 400 satırlık "temizlik + özellik + migrate" incelenmez, geçilir — ikisi de kötüdür.
- Açıklama, neden'i söyler. Diff zaten ne'yi gösterir.
- İnceleme yorumunu kişisel algılamayın. "Pushback" gerekçeliyse öğrenmedir; gerekçesizse tartışın.
- CI kırmış PR incelemeye sunulmaz. İnceleyenin zamanı derleyici değildir.
- LGTM + 800 satır (last-good-to-merge, last-glance-to-merge).
- Yalnızca stil yorumu, sıfır davranış sorusu.
- "Ben olsa şöyle yazardım" — daha iyi değil, farklı.
- Onayın tek kişiye yığılması; o kişi tatile çıkınca iş durur.
- "Acil" gerekçesiyle incelemesiz
main. Acil bir prosedürdür; yokluk değildir. En az bir ikinci göz + geri alma planı.
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.
picklebellek ve kod çalıştırabilir. Yalnızca sizin ürettiğiniz, güvenilir bir bayt akışı için. Kullanıcı yüklemesi, kuyruk mesajı, önbellek — varsayılanınız JSON veya mesaj sınırlı bir şema (Pydantic, msgspec) olsun.yaml.load(PyYAML) tarihsel olarak keyfi nesne üretir.safe_loadveya başka bir güvenli yükleyici.eval,exec,compilekullanıcı metninde yok. Dinamik import bile "eklenti adı beyaz liste" olmadan tehlikelidir.
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:
- ya her asenkron fonksiyon
*_async(açık, biraz gürültülü), - ya da asenkron paketlerde her I/O asenkrondur, sonek gereksizdir.
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.
- Doğru yazın.
- Testle kilitleyin.
- Yavaşsa ölçün (
python -m cProfile,py-spy,timeit, gerçek trafik). - Algoritmayı veya I/O'yu düzeltin (O(n²) → O(n), N+1 sorgu, senkron çağrı döngüde).
- Hâlâ yetmiyorsa mikro iyileştirme ve ancak o zaman okunabilirlikten bilinçli feragat.
"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 total- Okunmaz bir cache katmanı, hit oranını ölçmeden.
__slots__her dataclass'te "olur da bellek..." diye.- Tek kullanımlık betikte thread pool.
listyerinearray.arrayçünkü "daha C gibi".
Okunabilirlik, 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.
- İsimler gerçeği söylüyor; yerleşik gölgelenmemiş (
id,list,type). - Boolean'lar olumlu (
is_,has_,can_);== Trueyok. - Herkese açık fonksiyonlarda tip ipucu var.
- 4+ parametre bir nesneye dönüşmüş veya gerekçeli.
- Koruma cümleleri mutlu yolu düz bırakıyor.
-
except:yok; yakalanan tip yönetiliyor. - İstisna zinciri (
from) kopmamış. -
None/ boş / 0 ayrımı bilinçli (is Nonevsif not xs).
- Sihirli sayı ve durum dizgisi isimlendirilmiş veya
Enum. - Para
Decimal;floatdeğil. - Değişebilir varsayılan argüman yok.
- Tekrar bilgi tekrarı mı, yoksa zararsız benzerlik mi? (DRY vs AHA)
- Fonksiyon tek seviyede tek iş (orkestrasyon dahil).
- Sınıf gerçekten durum+davranış taşıyor; Java töreni değil.
- Yeni bağımlılık dışarıdan enjekte edilmiş, global değil.
- Modül bir cümleyle anlatılabiliyor.
-
printüretim yolunda yok; log seviyeli ve bağlamlı. - Sır, jeton, PII logda ve kaynakta yok.
- SQL/kabuk/yol kullanıcı metniyle birleştirilmemiş.
- Davranış testi var (mutlu yol + en az bir red / kenar).
-
ruff checkveruff format --checkgeçiyor. - Tip denetimi (projede açıksa) geçiyor.
- PR yalnızca bir amaç taşıyor (özellik veya refactor).
-
asynciçinde bloklayan I/O yok (veyato_threadile işaretli). - Zaman aşımı var;
CancelledErroryutulmuyor.
- Robert C. Martin — Clean Code. İlkeler evrenseldir; örnekler Java ağırlıklıdır. Bu rehber o ilkeleri Python'a taşır, tapınaklaştırmaz.
- Martin Fowler — Refactoring. Dönüşüm katalogu, koku listesi.
- David Thomas, Andrew Hunt — The Pragmatic Programmer. İzci kuralı, kırık pencere, evrensel hijyen.
- Luciano Ramalho — Fluent Python. Dilin gerçekten sunduğu araçlar; "Pythonic"in kaynağı.
- Brett Slatkin — Effective Python. Madde madde, kanıtlı öneriler.
- PEP 8 — stil
- PEP 20 — Zen of Python
- PEP 257 — docstring
- PEP 484 ve sonrası — tip ipuçları
- PEP 621 —
pyproject.tomlmetadata - PEP 636 —
matchöğreticisi
PEP metinleri resmi olarak İngilizcedir; çeviri varsa yardımcı, asıl sözleşme İngilizce sürümdür.
- Ruff — lint + format
- uv — ortam ve paket
- pytest — test
- pyright / ty — tip
- pre-commit — commit kapısı
- Refactoring Guru — desenler ve kokular, dil bağımsız
| 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