Witajcie!
Zaczynam serię trzech postów o debugowaniu w Pythonie. Przez lata pracy z Pythonem i Django widziałem setki błędów - od prostych literówek po skomplikowane race conditions. Ale to, co mnie najbardziej zaskoczyło podczas pracy ze społecznością PyMasters, to ile problemów mają nawet doświadczeni programiści z systematycznym podejściem do debugowania.
Dlatego postanowiłem napisać przewodnik - od podstaw po zaawansowane techniki. Ten pierwszy post to fundamenty: dlaczego debugowanie jest trudne, jak tego unikać i jakie narzędzia używać.
Dlaczego debugowanie tak boli?
Efekt "czarnej skrzynki" we własnym kodzie
Paradoks debugowania polega na tym, że własny kod staje się dla nas zagadką. Piszesz funkcję, testujesz ją, wszystko działa. Tydzień później - bum, błąd. Patrzysz na swój kod jak na hieroglify.
To nie jest Twoja wina. To normalne. Według badań ACM, około 70% działań deweloperów jest związanych z co najmniej jednym błędem poznawczym (ref: https://cacm.acm.org/research/cognitive-biases-in-software-development/).
# Pisałeś to miesiąc temu i wydawało się oczywiste
def calculate_total_price(items, discount=0.1):
total = 0
for item in items:
if item.get('special_offer'):
# Co oznaczała ta logika?
price = item['price'] * (1 - discount) * 0.9
else:
price = item['price'] * (1 - discount)
total += price
return total
# Teraz patrzysz i myślisz: "Co tu się dzieje?"Cognitive bias - dlaczego mózg nas oszukuje
Efekt Dunninga-Krugera to złudzenie poznawcze, w którym osoby z małą wiedzą w danej dziedzinie przeceniają swoje umiejętności, bo nie potrafią dostrzec własnych braków. Uderza szczególnie mocno w debugowaniu. Początkujący programiści często są przekonani, że świetnie radzą sobie z naprawianiem błędów, choć faktycznie oceniają się nawet o 40–50 miejsc wyżej w rankingu niż wynika z ich realnych umiejętności.
To jednak nie jest tylko problem juniorów — także bardziej doświadczeni programiści mogą mieć zniekształcony obraz własnych kompetencji: czasem je przeceniają, a czasem wręcz zaniżają, myśląc, że skoro coś jest dla nich proste, to inni też to potrafią.
Confirmation bias (efekt potwierdzenia) to prawdziwy zabójca. Testujesz kod tak, żeby potwierdzić swoje oczekiwania, zamiast próbować go "zepsuć".
def validate_email(email):
# Test który robiłeś
return "@" in email and "." in email
# Co Ty sprawdzałeś:
print(validate_email("[email protected]")) # True - git!
# Co powinieneś sprawdzić:
print(validate_email("@.")) # True - ups!
print(validate_email("jacek@@.com")) # True - też ups!
print(validate_email("")) # False - okBrak systematycznego podejścia
Najgorsze podejście to "metoda random changes". Zmieniam coś, sprawdzam czy działa, jak nie to zmieniam coś innego. To recepta na katastrofę. I gonienie w kółko.
Ale jest nadzieja. Z dobrą metodologią można to znacznie skrócić.
Jak walczyć z bólem debugowania?
Złote zasady
- Załóż że to TY zrobiłeś błąd - nawet jeśli "to niemożliwe". Komputer się nie myli - robi, co mu każesz.
- Używaj naukowej metody - hipoteza → eksperyment → wnioski.
- Pisz notatki podczas debugowania. Zwłaszcza te z punktu drugiego.
- Co 30 minut zrób przerwę - świeże spojrzenie pomaga.
Cykl życia błędu w praktyce
Każdy bug przechodzi przez te same etapy:
1. Zgłoszenie: "Gdy kliknę 'zapisz' po raz drugi, formularz się wywala"
2. Reprodukcja: Najważniejszy krok. Bez reprodukcji nie ma debugowania.
# Dobry opis reprodukcji:
"""
1. Otwórz formularz dodawania produktu
2. Wypełnij wszystkie pola
3. Kliknij "Zapisz" - działa
4. Bez odświeżania strony kliknij "Zapisz" ponownie
5. Błąd: ValidationError: slug już istnieje
"""3. Izolacja: Stwórz minimalny przypadek testowy. To kluczowe.
4. Fix + testy: Napraw i dodaj test, żeby się nie powtórzyło.
Print vs Logging - kiedy używać którego?
Kiedy używać print()
Tylko w trzech przypadkach:
- Szybkie debugowanie jednorazowych skryptów.
- Komunikaty help dla aplikacji CLI.
- Eksploracja w Jupyter notebookach.
# OK dla szybkiego testu
def test_function():
data = fetch_data()
print(f"Fetched {len(data)} items") # Szybki debug
return process_data(data)Logging. Co to i kiedy używać?
Logging to system do rejestrowania zdarzeń z hierarchią poziomów (DEBUG, INFO, WARNING, ERROR, CRITICAL) i możliwością konfiguracji handlerów - gdzie logi idą (konsola, plik, sieć). W przeciwieństwie do print(), możesz kontrolować co się loguje, formatować wiadomości ze znacznikami czasowymi i kierować różne poziomy do różnych miejsc.
Kiedy używać logging?
W każdym innym przypadku. Seriously.
import logging
# Nowoczesna konfiguracja dla Python 3.12
logging.basicConfig(
level=logging.INFO,
format='{asctime} - {name} - {levelname} - {message}',
style='{', # Nowsze formatowanie
datefmt='%Y-%m-%d %H:%M:%S',
encoding='utf-8',
handlers=[
logging.StreamHandler(),
logging.FileHandler('app.log', encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
def process_user_data(user_id):
logger.info("Processing user %s", user_id)
try:
user = get_user(user_id)
logger.debug("User data: %s", user.email)
except UserNotFound:
logger.error("User %s not found", user_id)
raise
except Exception as e:
logger.critical("Unexpected error for user %s: %s", user_id, e)
raiseDlaczego logging wygrywa:
- Poziomy ważności (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- Formatowanie z timestampami
- Możliwość wyłączania/filtrowania
- Zapisywanie do plików z rotacją
- Integracja z systemami monitorowania
breakpoint() - nowa era debugowania
Od Python 3.7 zapomnij o pdb.set_trace(). Używaj breakpoint().
def calculate_fibonacci(n):
if n <= 0:
return []
elif n == 1:
return [0]
breakpoint() # Zatrzyma się tutaj
sequence = [0, 1]
for i in range(2, n):
next_num = sequence[i-1] + sequence[i-2]
sequence.append(next_num)
return sequence
# Kontrola przez environment:
# PYTHONBREAKPOINT=0 python script.py # Wyłącza wszystkie
# PYTHONBREAKPOINT=ipdb.set_trace python script.py # Używa ipdbPodstawowe komendy PDB
Gdy już jesteś w debuggerze:
| Komenda | Co robi | Przykład |
|---|---|---|
n | Następna linia | n |
s | Wejdź do funkcji | s |
c | Kontynuuj | c |
p zmienna | Wydrukuj wartość | p sequence |
pp zmienna | Pretty-print | pp complex_dict |
l | Pokaż kod | l |
w | Stack trace | w |
# Przykład sesji debugowania
def problematic_function(data):
breakpoint()
result = []
for item in data:
# W debuggerze możesz:
# (Pdb) p item # Zobacz wartość
# (Pdb) p type(item) # Sprawdź typ
# (Pdb) n # Następna linia
processed = item * 2
result.append(processed)
return resultVS Code Debugger - konfiguracja dla Django
Zapomnij o print() i pdb w VS Code. Debugger to game changer.
Konfiguracja .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Django: Debug Server",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": [
"runserver",
"--noreload",
"--nothreading"
],
"django": true,
"autoReload": {
"enable": true
},
"justMyCode": true,
"console": "integratedTerminal"
}
]
}Jak używać
- Ustaw breakpoint klikając na lewy margines (czerwona kropka)
- Uruchom debugger (F5)
- Otwórz przeglądarkę i idź do swojej strony
- VS Code zatrzyma się na breakpoint
Pro tips:
- Conditional breakpoints: Prawy klik → "Add Conditional Breakpoint" →
user.id == 123 - Logpoints: Zamiast breakpoint, wypisz wartość bez zatrzymywania
- Watch expressions: Monitoruj zmienne w czasie rzeczywistym
# views.py
def user_profile(request, user_id):
# Ustaw breakpoint tutaj ↓
user = get_object_or_404(User, id=user_id)
# W debuggerze zobaczysz:
# - request.GET, request.POST
# - user_id (z URL)
# - Cały kontekst Django
context = {'user': user}
return render(request, 'profile.html', context)Top 5 błędów w Pythonie i jak je naprawić
1. AttributeError - "ma" czy "nie ma"?
# Błąd
user = get_user_or_none(123)
print(user.name) # AttributeError jeśli user to None
# Rozwiązanie 1: Sprawdzenie
if user:
print(user.name)
else:
print("User not found")
# Rozwiązanie 2: getattr z defaultem
print(getattr(user, 'name', 'Unknown'))
# Rozwiązanie 3: hasattr
if hasattr(user, 'name'):
print(user.name)2. KeyError - klucz, który nie istnieje
data = {'name': 'Jacek', 'age': 45}
# Błąd
print(data['email']) # KeyError
# Rozwiązania
print(data.get('email', 'No email')) # Bezpieczne z defaultem
print(data.get('email')) # Bezpieczne, zwróci None
# Sprawdzenie istnienia
if 'email' in data:
print(data['email'])3. IndentationError - Python jest wybredny
# Błąd
def my_function():
print("Hello") # Brak wcięcia
# Poprawka
def my_function():
print("Hello") # 4 spacje (PEP 8)
# Mieszanie spacji i tabów - to katastrofa
def bad_function():
if True:
print("Tab") # Tab
print("Spaces") # 4 spacje - IndentationError!4. TypeError - nie ten typ
# Błąd
result = "5" + 3 # TypeError: can't concatenate str and int
# Rozwiązania
result = int("5") + 3 # Explicit conversion
result = "5" + str(3) # Lub w drugą stronę
# Bezpieczna konwersja
def safe_int(value, default=0):
try:
return int(value)
except (ValueError, TypeError):
return default
number = safe_int(user_input, 0)5. Race Conditions - threading może boleć
import threading
from time import sleep
counter = 0
# Błędny kod - race condition
def increment():
global counter
temp = counter # Krok 1: czytaj
temp += 1 # Krok 2: modyfikuj
sleep(0.01) # Krok 3: inne wątki mogą się wmieszać!
counter = temp # Krok 4: zapisz
# Bezpieczny kod z lock'iem
lock = threading.Lock()
def increment_safe():
global counter
with lock: # Tylko jeden wątek na raz
temp = counter
temp += 1
sleep(0.01) # Teraz bezpieczne
counter = temp
# Test
threads = [threading.Thread(target=increment_safe) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Counter: {counter}") # Zawsze 10Logging w Django - konfiguracja, której potrzebujesz
Django ma wbudowane wsparcie dla logging. Oto konfiguracja, która faktycznie działa:
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOGS_DIR = BASE_DIR / 'logs'
LOGS_DIR.mkdir(exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {asctime} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
# Kluczowe: RotatingFileHandler
'rotating_file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': LOGS_DIR / 'django_app.log',
'maxBytes': 1024*1024*5, # 5 MB
'backupCount': 5, # 5 kopii zapasowych
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console', 'rotating_file'],
'level': 'INFO',
'propagate': False,
},
'myapp': { # Twoja aplikacja
'handlers': ['console', 'rotating_file'],
'level': 'DEBUG',
'propagate': False,
},
},
}
# views.py
import logging
logger = logging.getLogger(__name__)
def my_view(request):
logger.info("User %s accessed view", request.user.id)
try:
# Twoja logika
result = do_something()
logger.debug("Operation completed successfully")
return JsonResponse({'status': 'success'})
except Exception as e:
logger.error("Error in my_view: %s", e, exc_info=True)
return JsonResponse({'error': 'Something went wrong'}, status=500)Dlaczego RotatingFileHandler?
- Automatycznie tworzy nowe pliki, gdy obecny osiągnie limit
- Usuwa stare logi automatycznie
- Zapobiega zapełnieniu dysku
- NIGDY nie loguj bezpośrednio do pliku na produkcji bez rotacji. Skończy się miejsce na dysku na produkcji.
Narzędzia które oszczędzą Ci godziny
pyflakes - znajdź błędy bez uruchamiania kodu
pip install pyflakes
pyflakes my_file.py# problematic_code.py
import sys
import os # Nieużywany import
import json
def calculate_area(radius):
return 3.14 * raduis ** 2 # Błąd w nazwie zmiennej
# pyflakes wyłapie:
# Line 2: 'os' imported but unused
# Line 6: undefined name 'raduis'black - sformatuj kod automatycznie
pip install black
black my_file.py # Sformatuje w miejscu
black --check my_file.py # Sprawdzi bez zmian# Przed black
def my_function(x,y,z):
return x+y+z
# Po black
def my_function(x, y, z):
return x + y + zmypy - sprawdzanie typów
pip install mypy
mypy my_file.pydef greet(name: str) -> str:
return f"Hello, {name}!"
def add_numbers(a: int, b: int) -> int:
return a + b
# To spowoduje błąd mypy
result = add_numbers("5", "10") # Error: Expected int, got str
print(greet(123)) # Error: Expected str, got intRubber Duck Debugging - nie żartuje, to działa
Seriously. Miej gumową kaczkę na biurku i tłumacz jej swój kod.
Dlaczego to działa psychologicznie:
- Zmiana perspektywy: Wychodzisz poza swoje założenia
- Werbalizacja: Mówienie jest wolniejsze od myślenia
- Metakognicja: Myślisz o swoim myśleniu
Proces:
- Wyjaśnij problem ogólnie
- Przejdź przez kod linia po linii
- Opisz co każda linia powinna robić
- Sprawdź czy faktycznie to robi
Często podczas wyjaśniania znajdziesz błąd. Wielokrotnie prosiłem kolege na slacku o pomoc. I jak już opisałem problem, wysłałem, kolega odpisuje "zaraz". Najczęściej jak wrócił do mnie to moja odpowiedź była "Już nie trzeba". Sam proces opisania problemu powodował znalezienie rozwiązania.
Podsumowanie
Debugowanie to nie kara za błędy - to normalna część programowania.
Kluczowe wnioski:
- Używaj
breakpoint()zamiastprint() - Konfiguruj logging od początku projektu
- VS Code debugger > pdb dla Django
- Systematyczne podejście > random changes
- Narzędzia jak pyflakes/black/mypy oszczędzą Ci godziny
W następnym poście pokażę Ci metodę naukową w debugowaniu - jak podejść do błędu jak detektyw, żeby znaleźć go szybko i skutecznie.
Spodobał Ci się post? Sprawdź pozostałe części serii lub podziel się nim!
Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!
Zapraszam do zadawania pytań przez formularz kontaktowy. Pamiętaj, że jeśli potrzebujesz wsparcia, możesz napisać do mnie - pomogę.
Dla początkujących programistów przygotowałem darmowego ebooka "WARSZTAT JUNIORA: Przewodnik po kluczowych kompetencjach i narzędziach dla początkującego programisty Pythona".