Interactive course Backend

Praktyczny Bootcamp Pythona — Od Zera do Automatyzacji

Buduj prawdziwe narzędzia w Pythonie, we właściwy sposób

22 lessons 7 chapters 1 min

Witaj w Praktycznym Bootcampie Pythona

Od zera do automatyzacji

Niezależnie od tego, czy piszesz właśnie swoją pierwszą linijkę kodu, czy jesteś doświadczonym programistą C# / Java zmęczonym boilerplate'em i ciekawym czystej, nowoczesnej składni Pythona — trafiłeś we właściwe miejsce.

Większość kursów Pythona albo traktuje cię jak dziecko, albo zakłada, że masz już doktorat z informatyki. Ten kurs jest inny. Pomijamy teoretyczne ozdobniki i budujemy prawdziwe, nowoczesne narzędzia — od interaktywnych skryptów terminalowych po aplikację CLI napędzaną sztuczną inteligencją — i uczymy nie tylko jak pisać kod w Pythonie, ale jak pisać go dobrze i wydajnie.

Jak korzystać z tego kursu

Kurs napisany jest dla dwóch typów czytelników:

  • Zupełnie początkujący. Czytaj od deski do deski. Każda lekcja opiera się na poprzedniej, a my krok po kroku przeprowadzimy cię przez konfigurację i logikę.
  • Programista przechodzący z innego języka. Wypatruj ramek 🛑 Notka dla programistów. To skondensowane podsumowania tego, czym Python różni się od języków silnie typowanych i kompilowanych — typowanie, zasięg, pamięć, narzędzia — żebyś mógł przelecieć przez podstawy i od razu zacząć budować.

Każda lekcja kończy się krótkim quizem. Zdobądź co najmniej 67%, aby odblokować następną.

Co zbudujesz

Każdy rozdział zakończysz czymś, co działa: kalkulatorem napiwków, grą w zgadywanie liczby, systemem ekwipunku, analizatorem logów, monitorem floty serwerów, inspektorem repozytoriów GitHuba na żywo, a na końcu otypowanym, przetestowanym narzędziem CLI napędzanym AI.

Zrób sobie kawę, otwórz terminal i zaczynamy.

Lekcja 1 · Wprowadzenie

Konfiguracja i model myślenia w Pythonie

⏱ ~20 min 🎯 Początkujący 4 sekcji + quiz

Dlaczego Python

Python traktuje czas programisty jako droższy niż czas maszyny. Przestajesz walczyć ze składnią i zaczynasz rozwiązywać problem. Działa wszędzie, ma ogromną bibliotekę standardową ("baterie w zestawie") i największy ekosystem pakietów zewnętrznych ze wszystkich języków — frameworki webowe, narzędzia do danych, uczenie maszynowe, automatyzację.

Ten kurs przeprowadzi cię od pierwszej linijki kodu do prawdziwego, otypowanego, przetestowanego narzędzia wiersza poleceń napędzanego AI. Nie wypiszemy "Hello World" i nie zakończymy na tym dnia — już w pierwszym rozdziale zbudujesz działający kalkulator, który pobiera dane i liczy.

💡 Wskazówka: Programowania uczysz się pisząc kod, a nie czytając o nim. Trzymaj otwarty terminal i uruchamiaj każdy fragment. Psuj je celowo i patrz, co się dzieje.

Trzy rzeczy do zapamiętania

🛑 Notka dla programistów: Model myślenia w Pythonie

Przechodzisz z C#, .NET albo Javy? Dodanie Pythona do warsztatu wydaje się jak kod na nieśmiertelność, ale architektura różni się na trzy sposoby, które warto sobie przyswoić od razu:

  • Brak kroku kompilacji. Python jest interpretowany linijka po linijce. Piszesz, uruchamiasz. Nie ma budowania.
  • Typowanie dynamiczne. Nie deklarujesz string name ani int age. Zmienne dostają typ w czasie działania, na podstawie tego, co przypiszesz.
  • Białe znaki mają znaczenie. Python używa wcięć zamiast {} do oznaczania bloków kodu. Źle wcięty kod Pythona dosłownie się nie uruchomi — co wymusza, by każdy czytany kod był wizualnie spójny.

Trzymaj się tych trzech idei, a większość "niespodzianek" Pythona przestaje zaskakiwać.

Konfiguracja w 60 sekund

Krok 1 — interpreter.

  • Linux (Arch / EndeavourOS): sudo pacman -S python
  • Linux (Debian / Ubuntu): sudo apt install python3 python3-pip
  • macOS (Homebrew): brew install python
  • Windows: pobierz instalator ze python.org. Koniecznie zaznacz "Add Python to PATH" na pierwszym ekranie — pominięcie tego pola to najczęstszy powód, dla którego instalacja na Windowsie "nie działa".

Krok 2 — edytor.

  • Visual Studio Code — lekki, szybki, skupiony na terminalu. Zainstaluj oficjalne rozszerzenie "Python" od Microsoftu.
  • JetBrains PyCharm — jeśli żyjesz w ekosystemie JetBrains (Rider, IntelliJ), PyCharm będzie jak w domu.

Sprawdź instalację:

python --version
# Wynik powinien wyglądać mniej więcej tak: Python 3.12.x

Jeśli widzisz numer wersji, jesteś gotowy. (Na niektórych systemach Linux polecenie to python3.)

Dwa sposoby uruchamiania kodu

REPL (Read-Eval-Print Loop) to interaktywna piaskownica. Wpisz python bez pliku, a dostaniesz znak zachęty >>>, który uruchamia kod w miarę pisania:

>>> 2 + 2
4
>>> "py" * 3
'pypypy'

Idealny do szybkich eksperymentów. Wpisz exit(), żeby wyjść.

Skrypt to plik .py, który uruchamiasz z terminala. Tak pisze się i udostępnia prawdziwe programy:

# hello.py
print("Hello from a script!")
python hello.py
# Hello from a script!

print() to wbudowana funkcja, która wypisuje tekst na ekran. REPL automatycznie pokazuje wartość wyrażenia; skrypt nie — w skrypcie musisz print()-ować wszystko, co chcesz zobaczyć.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 2 · Podstawy

Zmienne, typy i f-stringi

⏱ ~22 min 🎯 Początkujący 4 sekcji + quiz

Nazywanie wartości

Utworzenie zmiennej to po prostu nadanie jej nazwy i przypisanie wartości znakiem =:

target_ip = "192.168.1.15"  # string (łańcuch znaków)
port = 8080                 # liczba całkowita
is_secure = True            # wartość logiczna

Nigdzie nie powiedzieliśmy, jakiego typu jest każda zmienna. Python wyliczy to z wartości. Konwencja nazw to snake_case — małe litery łączone podkreśleniami.

💡 Wskazówka: Nazwy powinny opisywać co znaczy wartość, a nie jakiego jest typu. user_count jest lepsze niż n; is_secure lepsze niż flag.

Typy są przypisane do wartości, nie do nazw

🛑 Notka dla programistów: Typowanie dynamiczne kontra statyczne

W C#/Javie piszesz int age = 34;zmienna ma na zawsze ustalony typ. W Pythonie typ niesie wartość, a nazwę można podpiąć pod wartość dowolnego typu:

x = 10        # x wskazuje na int
x = "ten"     # teraz ta sama nazwa wskazuje na str — całkowicie poprawne

Wyzwalające, ale oznacza też, że literówka w nazwie tworzy nową zmienną zamiast błędu kompilacji. Tę siatkę bezpieczeństwa odzyskamy dzięki adnotacjom typów i mypy w rozdziale 7.

Jeśli kiedyś musisz sprawdzić typ, wbudowane type() ci go poda:

print(type(port))  # <class 'int'>

Podstawowe wbudowane typy, które poznasz najpierw: int, float, str, bool.

Praca z tekstem

Możesz używać pojedynczych ' lub podwójnych " cudzysłowów zamiennie — wybierz jeden i bądź konsekwentny. Stringi obsługują przydatne operatory:

name = "Ada"
print(name + " Lovelace")   # łączenie    -> "Ada Lovelace"
print("=" * 20)             # powielanie  -> "===================="
print(len(name))            # długość     -> 3

Dla tekstu obejmującego wiele linii użyj potrójnych cudzysłowów:

banner = """
Welcome to the system.
All activity is logged.
"""

f-stringi: jedyny słuszny sposób formatowania

Postaw f przed otwierającym cudzysłowem, a potem wrzucaj zmienne prosto do tekstu w nawiasach klamrowych {}:

username = "admin"
login_attempts = 3

# Stary, niezgrabny sposób — ręczne łączenie i konwersja przez str():
print("User " + username + " failed to login " + str(login_attempts) + " times.")

# Sposób z f-stringiem:
print(f"User {username} failed to login {login_attempts} times.")

Oba wypisują to samo. Tylko jeden warto pisać.

W klamrach możesz umieścić dowolne wyrażenie, a po dwukropku dodać specyfikator formatu:.2f pokazuje dwa miejsca po przecinku, świetne do pieniędzy:

price = 144.6 / 3
print(f"Each person pays: ${price:.2f}")   # Each person pays: $48.20

Bez :.2f arytmetyka zmiennoprzecinkowa pokaże brzydkie cyfry jak 48.199999999999996.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 3 · Podstawy

Liczby, matematyka i dane wejściowe

⏱ ~25 min 🎯 Początkujący 4 sekcji + quiz

Python to zdolny kalkulator

OperacjaOperatorUwaga
Dodawanie / odejmowanie+ -
Mnożenie*
Dzielenie/Zawsze zwraca float: 10 / 2 to 5.0
Dzielenie całkowite//Odrzuca resztę: 7 // 2 to 3
Modulo (reszta)%7 % 2 to 1
Potęgowanie**2 ** 10 to 1024

Dwa operatory zaskakują początkujących: / zawsze daje float, a % (modulo) daje resztę — nieoceniona przy "czy to parzyste?" (n % 2 == 0) albo "co dziesiąty element" (i % 10 == 0).

Jest też przypisanie z operacją — x += 1 to skrót od x = x + 1. Działa tak samo dla -=, *=, /=. += będziesz używać bez przerwy.

input() i pułapka konwersji

Funkcja input() wstrzymuje program i czeka, aż użytkownik coś wpisze:

age = input("How old are you? ")

Pułapka: input() zawsze zwraca string. Jeśli użytkownik wpisze 34, Python widzi "34". Spróbuj policzyć — age + 5 — a skrypt się wywróci, bo nie da się dodać liczby do kawałka tekstu.

Rozwiązaniem jest konwersja stringa na liczbę: int() dla liczb całkowitych, float() dla dziesiętnych.

age = int(input("How old are you? "))
print(f"In 10 years, you will be {age + 10}.")

⚠️ Uwaga: Jeśli użytkownik wpisze "twenty", int() zgłosi ValueError i skrypt się wywróci. Na razie to w porządku — eleganckie obsłużenie tego to dokładnie temat rozdziału 5 (obsługa błędów).

Zamiana między typami

Funkcje konwertujące zamieniają jeden typ na drugi:

int("42")      # 42    string -> int
float("3.14")  # 3.14  string -> float
str(99)        # "99"  int    -> string
int(3.99)      # 3     float  -> int (ucina, NIE zaokrągla)

Dwie rzeczy do zapamiętania:

  • int() na float ucina w stronę zera — int(3.99) to 3, nie 4. Użyj round(3.99), jeśli chcesz zaokrąglić.
  • Konwersja zawodzi głośno na bzdurach: int("hello") zgłasza ValueError. To zaleta — mówi ci, że dane były błędne, zamiast po cichu zgadywać.

Łączymy to w całość

Połączmy zmienne, f-stringi, matematykę, input i konwersję w prawdziwe narzędzie. Utwórz plik calculator.py:

# 1. Przywitaj użytkownika
print("Welcome to the Terminal Tip & Split Calculator!")

# 2. Pobierz dane i od razu je skonwertuj.
#    float(), bo pieniądze mają grosze:
bill_amount = float(input("Enter the total bill amount: $"))

# Dzielenie przez 100 zamienia 20 na 0.20 — tego potrzebuje obliczenie:
tip_percentage = float(input("Enter the tip percentage (e.g. 15, 20): ")) / 100

# Ludzie liczą się w całościach, więc int():
people = int(input("How many people are splitting the bill? "))

# 3. Wykonaj obliczenia
tip_amount = bill_amount * tip_percentage
total_bill = bill_amount + tip_amount
cost_per_person = total_bill / people

# 4. Wypisz czytelny paragon. \n dodaje pustą linię; :.2f zaokrągla do groszy.
print("\n--- Receipt ---")
print(f"Total Bill (with tip): ${total_bill:.2f}")
print(f"Each person pays: ${cost_per_person:.2f}")
python calculator.py

Nie wypisałeś "Hello World" — zbudowałeś skrypt obsługujący prawdziwe dane i prawdziwą matematykę. Zauważ, jak jest jeszcze kruchy: wpisz "twenty", a się wywróci. W następnym rozdziale dodamy mózg — decyzje i pętle — żeby kod reagował, zamiast ślepo ufać.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 4 · Sterowanie

Wartości logiczne i instrukcje warunkowe

⏱ ~22 min 🎯 Początkujący 4 sekcji + quiz

True i False

W sercu całej logiki leży wartość logiczna: True lub False (zawsze z wielkiej litery w Pythonie). Wartości logiczne tworzysz operatorami porównania:

OperatorZnaczy
==równe
!=różne
> <większe / mniejsze niż
>= <=większe-lub-równe / mniejsze-lub-równe
print(5 > 3)        # True
print(10 == "10")   # False — int nigdy nie jest równy str

⚠️ Uwaga: = przypisuje, == porównuje. Napisanie if x = 5: to błąd składni; chodziło ci o if x == 5:. To raz w życiu potyka każdego.

Rozgałęzianie

🛑 Notka dla programistów: Wcięcie to prawo

Przyzwyczajony do {} i ;? Python wyrzuca oba. Blok zaczyna się dwukropkiem :, a wszystko wcięte pod nim (cztery spacje, zgodnie z konwencją) należy do tego bloku. W momencie, gdy cofniesz wcięcie, blok się kończy.

system_status = "offline"
battery_level = 15

if system_status == "online":
    print("All systems nominal.")
elif battery_level < 20:
    print("Warning: low power. Initiating sleep mode.")
else:
    print("System is offline, but power is stable.")

elif to pythonowe "else if" — łącz ich tyle, ile chcesz. Python sprawdza warunki od góry do dołu i uruchamia pierwszy, który jest True, a resztę pomija.

Łączenie warunków

Wartości logiczne sklejasz słowami kluczowymi and, or i not (Python wypisuje je słownie — żadnych && ani ||):

age = 25
has_ticket = True

if age >= 18 and has_ticket:
    print("Entry granted.")

if not has_ticket:
    print("Please buy a ticket.")
  • andTrue tylko gdy obie strony są prawdą.
  • orTrue gdy którakolwiek strona jest prawdą.
  • not — odwraca wartość logiczną.

💡 Wskazówka: Python stosuje "ocenę skróconą" — w a and b, jeśli a jest fałszem, b nawet nie zostanie obliczone. Przydatne w strażnikach typu if user and user.is_admin:.

Pustka to False

Python elegancko podchodzi do pustki. Rzadko potrzebujesz if name != "" czy if len(items) > 0. Te wartości są falsy — traktowane jak False:

  • 0 i 0.0
  • "" (pusty string)
  • [], {}, (), set() (puste kolekcje)
  • None

Wszystko inne jest truthy. Możesz więc pisać:

user_input = ""

if not user_input:
    print("You didn't type anything!")

items = [1, 2, 3]
if items:                 # czyta się jak "jeśli są elementy"
    print(f"You have {len(items)} items.")

Ten idiom jest wszędzie w prawdziwym Pythonie. Przyjmij go — if items: jest czytelniejsze niż if len(items) > 0:.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 5 · Sterowanie

Pętle: while, for i range

⏱ ~26 min 🎯 Początkujący 5 sekcji + quiz

Powtarzaj, aż warunek się zmieni

Pętla while działa, dopóki jej warunek pozostaje True. Idealna do menu, pętli gry albo czegokolwiek, co ma trwać aż do konkretnego zdarzenia:

attempts = 0

while attempts < 3:
    attempts += 1
    print(f"Scanning... (scan {attempts})")

print("Done.")

⚠️ Uwaga: Jeśli warunek nigdy nie stanie się fałszem, dostajesz nieskończoną pętlę. Upewnij się, że coś w środku przesuwa ją ku wyjściu (tu: attempts += 1). Utknąłeś w jednej? Naciśnij Ctrl+C, żeby zatrzymać program.

Iterowanie po kolekcji

🛑 Notka dla programistów: Koniec z for (int i = 0; ...)

Przyzwyczajony do for (int i = 0; i < arr.Length; i++)? Pythonowe for zachowuje się jak foreach — iteruje wprost po elementach kolekcji, a nie po indeksie, którym zarządzasz ręcznie. Błędy "o jeden za dużo" w większości znikają.

# Przejdź po znakach stringa:
for letter in "HACK":
    print(f"Decrypting: {letter}")

# Przejdź po liście:
for ip in ["10.0.0.1", "10.0.0.2"]:
    print(f"Pinging {ip}")

Zmienna (letter, ip) przyjmuje po kolei każdą wartość. Pętla kończy się automatycznie, gdy kolekcja się wyczerpie.

Powtarzanie N razy i kierowanie pętlą

Aby powtórzyć ustaloną liczbę razy, połącz for z wbudowanym range():

# range(5) tworzy 0, 1, 2, 3, 4 — pięć liczb, zaczynając od zera.
for i in range(5):
    print(f"Ping {i + 1} sent.")

range(start, stop, step) jest elastyczny: range(2, 10, 2) daje 2, 4, 6, 8.

Dwa słowa kluczowe kierują każdą pętlą:

  • break — natychmiast wyjdź z pętli.
  • continue — pomiń resztę tej iteracji i wróć na górę.
for n in range(10):
    if n == 5:
        break       # zatrzymaj się całkiem na 5
    if n % 2 == 0:
        continue    # pomiń liczby parzyste
    print(n)        # wypisuje 1, 3

Pętla może mieć else

Mniej znana cecha: pętla for lub while może mieć blok else. Uruchamia się tylko gdy pętla zakończyła się normalnie — czyli nie została przerwana przez break. To elegancki sposób na wyrażenie "przeszukałem całość i nie znalazłem":

needle = 42
for n in [10, 20, 30]:
    if n == needle:
        print("Found it!")
        break
else:
    print("Not found anywhere.")   # uruchamia się, bo nie było break

Oszczędza to osobnej zmiennej-flagi found = False. W projekcie poniżej zobaczysz wersję z ręczną flagą — obie są poprawne; wybierz tę, która czyta się czytelniej.

Łączymy to w całość: gra w zgadywanie liczby

Potrzebujemy pierwszego kawałka biblioteki standardowej: modułu random. import-uj go na górze, a random.randint(a, b) da losową liczbę całkowitą z przedziału [a, b].

Utwórz plik firewall_bypass.py:

import random

# 1. Ustaw grę
secret_passcode = random.randint(1, 100)   # losowy int, 1..100 włącznie
max_attempts = 5
current_attempt = 0
firewall_bypassed = False

print("Welcome to the Terminal Hacker System.")
print(f"You have {max_attempts} attempts to bypass the firewall.\n")

# 2. Główna pętla gry
while current_attempt < max_attempts:
    current_attempt += 1
    guess = int(input(f"Attempt {current_attempt}: Enter your guess: "))

    # 3. Porównaj zgadnięcie z sekretem
    if guess == secret_passcode:
        print(f"[SUCCESS] Firewall bypassed in {current_attempt} attempts!")
        firewall_bypassed = True
        break
    elif guess < secret_passcode:
        print("[!] Too low.\n")
    else:
        print("[!] Too high.\n")

# 4. Obsłuż przegraną
if not firewall_bypassed:
    print(f"[LOCKED] The correct passcode was {secret_passcode}.")

Sprytny gracz złamie każdy kod w tym zakresie w 5 próbach strategią połówkową — zgaduj środek, za każdym razem odrzucaj połowę zakresu. To nie przypadek; matematykę O(log N) za tym stojącą poznamy w następnym rozdziale.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 6 · Dane

Stringi w głąb

⏱ ~24 min 🎯 Początkujący 4 sekcji + quiz

Stringi to sekwencje znaków

Każdy znak ma indeks, zaczynając od 0. Ujemne indeksy liczą się od końca:

text = "PYTHON"
print(text[0])    # 'P'  — pierwszy
print(text[-1])   # 'N'  — ostatni

Wycinanie (slicing) pobiera zakres przez [start:stop]start jest włączony, stop nie:

print(text[0:3])   # 'PYT'   indeksy 0,1,2
print(text[3:])    # 'HON'   od 3 do końca
print(text[:2])    # 'PY'    od początku do 2
print(text[::-1])  # 'NOHTYP' — zgrabny trik odwracania (krok -1)

💡 Wskazówka: To samo wycinanie [start:stop:step] działa na listach i krotkach — naucz się go raz, używaj wszędzie.

Nie da się zmienić stringa w miejscu

name = "ada"
name[0] = "A"        # 💥 TypeError — stringów nie da się modyfikować

Zamiast tego metody stringów zwracają nowy string, zostawiając oryginał nietknięty:

name = "ada"
proper = name.capitalize()   # "Ada"
print(name)                  # "ada"  — oryginał bez zmian

To często myli: name.upper() nie robi nic użytecznego, dopóki nie przechwycisz wyniku (name = name.upper() lub nie użyjesz go bezpośrednio). Oryginał nigdy się nie zmienia.

Skrzynka narzędziowa stringów

Garść metod pokrywa 90% prawdziwej pracy z tekstem:

MetodaCo robi
.lower() / .upper()zmienia wielkość liter
.strip()usuwa białe znaki z początku/końca
.replace(a, b)zamienia każde a na b
.split(sep)dzieli na listę wg sep (domyślnie: białe znaki)
.startswith(x) / .endswith(x)sprawdzenia logiczne
.find(x) / inznajdź / sprawdź podciąg
line = "  192.168.1.50 GET /index.html 200  "
clean = line.strip()                 # usuń otaczające spacje
parts = clean.split()                # ['192.168.1.50', 'GET', '/index.html', '200']
ip = parts[0]                        # '192.168.1.50'

print("GET" in clean)                # True
print(clean.endswith("200"))         # True

.split() to koń roboczy parsowania logów — będziesz go używać bez przerwy w tym kursie.

join() — właściwy sposób składania tekstu

Aby skleić listę stringów, użyj .join()nie pętli z +=:

servers = ["web-01", "web-02", "db-01"]

# ✅ Czysto i szybko:
print(", ".join(servers))   # "web-01, web-02, db-01"

Separator jest po lewej; iterowalny zbiór stringów wchodzi do join().

🛑 Notka dla programistów: Dlaczego nie += w pętli?

Budowanie dużego stringa przez powtarzane result += piece tworzy zupełnie nowy string w każdej iteracji (stringi są niezmienne), co daje pracę O(N²) — z tego samego powodu sięgasz po StringBuilder w C#/Javie. "".join(pieces) robi to w jednym przejściu. Użyj + dla paru stringów; użyj join() dla wielu.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 7 · Dane

Listy i krotki

⏱ ~24 min 🎯 Początkujący 4 sekcji + quiz

Uporządkowane, zmienne kolekcje

Lista to uporządkowana kolekcja. Tworzysz ją nawiasami kwadratowymi []. Listy są zmienne — możesz dodawać, usuwać i zmieniać elementy po utworzeniu.

loot = ["Gold", "Health Potion", "Iron Sword"]

# Dostęp przez indeks (od zera); ujemne liczą się od końca:
print(loot[0])    # Gold
print(loot[-1])   # Iron Sword

# Modyfikuj w miejscu:
loot.append("Magic Ring")        # dodaj na koniec
loot.remove("Iron Sword")        # usuń pierwsze pasujące
loot[1] = "Major Health Potion"  # podmień wg indeksu
loot.insert(0, "Map")            # wstaw na pozycji
popped = loot.pop()              # usuń i zwróć ostatni element

Listy to twój domyślny wybór dla danych uporządkowanych: kolejka zadań, log akcji, sekwencja wyników.

Codzienne narzędzia do list

nums = [4, 7, 1, 9, 2]

len(nums)            # 5   — ile elementów
sorted(nums)        # [1, 2, 4, 7, 9]  — zwraca NOWĄ posortowaną listę
nums.sort()          # sortuje nums W MIEJSCU, zwraca None
sum(nums)            # suma
min(nums), max(nums) # 1, 9
7 in nums            # True — test przynależności

nums + [100]         # łączenie -> nowa lista
[0] * 3              # [0, 0, 0]

⚠️ Uwaga: sorted(nums) zwraca nową listę; nums.sort() zmienia i zwraca None. Napisanie nums = nums.sort() to klasyczny błąd — właśnie ustawiłeś nums na None. Użyj sorted(), gdy chcesz kopię, a .sort(), gdy chcesz posortować w miejscu.

Niezmienne z założenia

Krotka wygląda jak lista, ale używa nawiasów () i jest niezmienna — raz utworzona, nie pozwala niczego dodać, usunąć ani zmienić:

server_coordinates = (192, 168, 1, 15)

print(server_coordinates[0])   # 192  — odczyt jest w porządku
server_coordinates[0] = 10     # 💥 TypeError — krotek nie da się zmieniać

Po co chcieć czegoś, czego nie da się zmienić? Dwa powody: niezmienność jest odrobinę szybsza i lżejsza oraz — co ważniejsze — to gwarancja. Wręczając komuś krotkę, obiecujesz, że dane nie zmienią się pod nogami. Używaj ich do stałych rekordów: współrzędnych, kolorów RGB, wiersza z bazy, konfiguracji, której nie wolno zmieniać w czasie działania.

💡 Wskazówka: Krotki błyszczą przy wielu zwracanych wartościach i rozpakowywaniu: x, y = (10, 20) przypisuje oba naraz. Funkcje, które "zwracają dwie rzeczy", w istocie zwracają jedną krotkę.

Która i kiedy?

Użyj listy, gdy…Użyj krotki, gdy…
kolekcja będzie się zmieniaćdane są stałe
kolejność ma znaczenie i będziesz edytowaćchcesz lekki rekord
to jednorodna sekwencja (wiele tego samego)to grupa o stałym rozmiarze z powiązanych pól

Dobra zasada z społeczności: listy są dla "wielu rzeczy tego samego rodzaju" (lista użytkowników), krotki są dla "jednej rzeczy o kilku częściach" ((imię, wiek, miasto) jednego użytkownika).

🛑 Notka dla programistów: Niezmienność jako kontrakt

Jeśli używałeś C# record lub struktury tylko do odczytu, krotka to lekka, bezimienna wersja: wartość, którą przekazujesz z obietnicą, że nikt jej nie zmieni. Gdy sięgniesz po wersję nazwaną i ustrukturyzowaną, to będzie dataclass — rozdział 6.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 8 · Dane

Słowniki i zbiory

⏱ ~26 min 🎯 Średniozaawansowany 4 sekcji + quiz

Magazyn klucz → wartość

Jeśli masz opanować tylko jedną strukturę danych w Pythonie, niech to będzie słownik (dict). Przechowuje dane jako pary klucz → wartość. Nawiasy klamrowe {}, z dwukropkiem między kluczem a wartością:

player = {
    "name": "Arthur",
    "level": 5,
    "class": "Knight",
}

print(player["class"])          # Knight  — wyszukaj po kluczu
player["level"] = 6             # klucz istnieje -> aktualizuje
player["guild"] = "The Ravens"  # klucz nowy -> tworzy

Wartością może być cokolwiek — liczba, string, lista, nawet inny słownik. Tak modelujesz zagnieżdżone dane bez wymyślania klasy do każdej drobnostki.

🛑 Notka dla programistów: To twój Dictionary<TKey,TValue> / HashMap — te same gwarancje tablicy mieszającej, znacznie mniej ceremonii.

Bezpieczny dostęp i iteracja

Wyszukanie brakującego klucza przez [] wywala się z KeyError. Użyj .get(), aby podać wartość zastępczą:

print(player.get("title"))            # None  — bez wywałki
print(player.get("title", "Squire"))  # "Squire"  — wartość domyślna

Iteruj po kluczach, wartościach lub po obu:

for key in player:                 # domyślnie klucze
    print(key)

for key, value in player.items():  # oba naraz — typowy przypadek
    print(f"{key} = {value}")

if "level" in player:              # przynależność sprawdza KLUCZE
    print("has a level")

.items() dające klucz i wartość razem to jeden z najczęściej używanych wzorców w całym Pythonie.

Nieuporządkowane, bez duplikatów

Zbiór używa nawiasów klamrowych {} i ma jedną supermoc: odrzuca duplikaty. Dodaj coś, co już tam jest, a zbiór cię po cichu zignoruje. Zbiory są też nieuporządkowane — nie polegaj na kolejności elementów.

visited = {"Tavern", "Forest", "Castle"}
visited.add("Tavern")    # już jest — ignorowane
visited.add("Dungeon")   # dodane
print("Forest" in visited)  # True

Zbiory świetnie nadają się do usuwania duplikatów i szybkich sprawdzeń "czy już to widziałem?". Robią też prawdziwą algebrę zbiorów:

a = {1, 2, 3}
b = {3, 4, 5}
print(a & b)   # {3}        część wspólna
print(a | b)   # {1,2,3,4,5} suma
print(a - b)   # {1, 2}     różnica

Decyzja wydajnościowa, która naprawdę się liczy

🛑 Notka dla programistów: Złożoność obliczeniowa (Big-O)

Jeśli musisz sprawdzić, czy element istnieje w dużej kolekcji — if item in collection:nie używaj listy. Lista skanuje każdy element po kolei: wyszukiwanie liniowe O(N).

set (i wyszukanie klucza w dict) to tablica mieszająca, więc przynależność jest O(1) — czas stały, niezależnie od rozmiaru. Parsujesz logi względem czarnej listy 500 000 złośliwych IP? Wczytaj je do zbioru, nie listy, a mozół zmieni się w coś natychmiastowego.

Ta sama idea napędza strategię połówkową z gry w zgadywanie: przepołowienie przestrzeni przeszukiwań na każdym kroku to O(log N), dlatego 5 prób pokrywa 100 liczb (a ~20 prób pokryłoby milion).

Struktura"czy x tu jest?"Trzyma kolejność?Duplikaty?
listO(N) wolnotaktak
setO(1) szybkonienie
dict (po kluczu)O(1) szybkotak (wstawiania)klucze unikalne

Wybór właściwego kontenera jest optymalizacją. Sięgaj po niego przed sprytnymi sztuczkami.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 9 · Dane

Iterowanie jak profesjonalista

⏱ ~24 min 🎯 Średniozaawansowany 4 sekcji + quiz

Potrzebujesz też indeksu? Użyj enumerate

Początkujący sięgają po ręczny licznik:

# ❌ Niezgrabnie:
i = 0
for item in items:
    print(i, item)
    i += 1

Python daje ci indeks i element razem przez enumerate():

# ✅ Pythonowo:
for i, item in enumerate(items):
    print(i, item)

Podaj start=1, aby liczyć po ludzku (1, 2, 3…) — idealne do numerów linii w pliku:

for line_number, line in enumerate(open("log.txt"), start=1):
    print(f"Line {line_number}: {line.strip()}")

Przechodzenie po dwóch listach równolegle

zip() paruje elementy z wielu iterowalnych obiektów, pozycja po pozycji:

names = ["web-01", "web-02", "db-01"]
loads = [0.45, 0.91, 0.30]

for name, load in zip(names, loads):
    print(f"{name}: {load}")

Zatrzymuje się na najkrótszym wejściu. zip() to też czysty sposób budowy słownika z dwóch list:

config = dict(zip(names, loads))
# {'web-01': 0.45, 'web-02': 0.91, 'db-01': 0.30}

Sortowanie po obliczonej wartości

sorted() (i .sort()) przyjmują funkcję key=, która mówi po czym sortować:

words = ["banana", "kiwi", "apple"]

print(sorted(words))                 # alfabetycznie: ['apple','banana','kiwi']
print(sorted(words, key=len))        # po długości:   ['kiwi','apple','banana']
print(sorted(words, reverse=True))   # odwrotna kolejność

Dla list słowników lub obiektów kierujesz klucz na pole, które cię interesuje (użyjemy do tego lambda w rozdziale 4). Ta sama idea key= napędza max() i min().

💡 Wskazówka: key=str.lower daje sortowanie bez rozróżniania wielkości liter — zgrabny trik.

Łączymy to w całość

Łączymy słowniki, zbiory i iterację w ekwipunek z gry tekstowej. Używamy słownika do przedmiotów składowalnych (liczy się ilość) i zbioru do przedmiotów jednorazowych.

Utwórz plik inventory.py:

inventory_counts = {        # przedmioty składowalne: nazwa -> ile
    "Health Potions": 3,
    "Gold Coins": 150,
}
unique_items = {"Rusty Key", "Map"}   # przedmioty unikalne
new_loot = ["Iron Shield", "Health Potion", "Map", "Health Potion"]

print("--- Player Inventory ---")
for item, count in inventory_counts.items():
    print(f"{item}: {count}")
print(f"Unique Items: {unique_items}\n")

print("--- Processing loot ---")
for item in new_loot:
    if item == "Health Potion":
        inventory_counts["Health Potions"] += 1
        print(f"[+] Health Potions -> {inventory_counts['Health Potions']}.")
    else:
        # Niech ZBIÓR sam wymusi unikalność:
        if item in unique_items:
            print(f"[!] Already have a {item}. Discarding duplicate.")
        else:
            unique_items.add(item)
            print(f"[+] Added {item}.")

print("\n--- Final Inventory ---")
print(f"Quantities: {inventory_counts}")
print(f"Unique Items: {unique_items}")

Zauważ, jak zbiór po cichu obsługuje zduplikowaną Map, podczas gdy słownik radośnie składuje mikstury. Wybór właściwej struktury sprawił, że logika niemal napisała się sama — dokładnie lekcja z sekcji o Big-O.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 10 · Funkcje

Funkcje, parametry i return

⏱ ~26 min 🎯 Początkujący 4 sekcji + quiz

def — pakowanie logiki do ponownego użycia

Kopiowanie tej samej logiki wszędzie to pułapka utrzymaniowa: zaostrz regułę w jednym miejscu, a musisz pamiętać o pozostałych czterech. Funkcja to nazwany, wielokrotnego użytku blok. Definiujesz go słowem def, nazwą w snake_case, nawiasami i dwukropkiem:

def boot_sequence():
    print("Initializing core modules...")
    print("System ready.")

# Definicja go nie uruchamia. Robi to WYWOŁANIE:
boot_sequence()

Gdy Python czyta blok def, po prostu zapamiętuje ciało — kod uruchamia się dopiero, gdy wywołasz funkcję po nazwie z ().

Podawanie danych

Nazwy w definicji to parametry; wartości, które przekazujesz, to argumenty:

def ban_user(username, reason):
    print(f"User {username} banned. Reason: {reason}")

ban_user("hacker99", "Exploiting bugs")
ban_user(reason="Spam", username="bot42")   # argumenty nazwane, dowolna kolejność

Parametry domyślne sprawiają, że argument jest opcjonalny:

def ping_server(ip, retries=3):
    print(f"Pinging {ip}... (retries: {retries})")

ping_server("10.0.0.1")              # używa domyślnej, 3
ping_server("10.0.0.5", retries=10)  # nadpisuje ją

⚠️ Uwaga: Parametry z wartościami domyślnymi muszą być po tych bez. I nigdy nie używaj zmiennej wartości domyślnej jak def f(items=[]) — ta jedna lista jest współdzielona przez wszystkie wywołania. Użyj None i utwórz świeżą w środku (więcej w rozdziale 6).

Rozróżnienie, które umyka początkującym

print() i return to nie to samo:

  • print() zrzuca tekst na konsolę, by przeczytał go człowiek.
  • return oddaje wartość z powrotem do programu, żeby inny kod mógł jej użyć.
def calculate_tax(subtotal):
    return subtotal * 1.08   # oddaj liczbę z powrotem

price = calculate_tax(50.00)   # przechwyć ją
print(f"Pay: ${price:.2f}")    # Pay: $54.00

Funkcja bez return zwraca w domyśle None. W momencie, gdy funkcja trafia na return, kończy działanie.

💡 Wskazówka: W swojej logice preferuj return nad print. Funkcję, która zwraca wynik, można użyć ponownie, łączyć i testować (rozdział 7); ta, która tylko wypisuje, jest ślepą uliczką.

Dokumentuj na bieżąco

Literał stringa tuż pod linią def to docstring — wbudowany w Pythona sposób dokumentowania, co robi funkcja. Edytory pokazują go po najechaniu; help() go odczytuje.

def analyze_strength(password):
    """Rate a password and return a human-readable verdict."""
    if len(password) < 8:
        return "WEAK - too short."
    return "OK"

Wyrób ten nawyk już teraz. Jednolinijkowy docstring mówiący co funkcja robi i co zwraca zwraca się przy pierwszym powrocie do kodu (twoim albo kolegi z zespołu).

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 11 · Funkcje

Zasięg i reguła LEGB

⏱ ~24 min 🎯 Średniozaawansowany 4 sekcji + quiz

Które zmienne widzi funkcja?

Python rozwiązuje nazwę, szukając jej kolejno w:

  • Local (lokalnym) — nazwy utworzone wewnątrz samej funkcji.
  • Enclosing (otaczającym) — nazwy w funkcji zewnętrznej, jeśli ta jest zagnieżdżona.
  • Global (globalnym) — nazwy zdefiniowane na najwyższym poziomie pliku.
  • Built-in (wbudowanym) — wstępnie wczytane nazwy Pythona, jak print i len.
system_status = "ONLINE"   # globalny

def check_system():
    status_code = 200       # lokalny
    print(f"{system_status} with code {status_code}")

check_system()
# print(status_code)   # 💥 NameError — lokalne giną, gdy funkcja kończy działanie

Funkcja może swobodnie czytać zmienną globalną; zmienne lokalne znikają w momencie zakończenia funkcji.

Pułapka dla programistów C#/Java

🛑 Notka dla programistów: Pułapka "zasięgu blokowego"

W C#/C++/Javie zmienna zadeklarowana wewnątrz if lub for ginie, gdy ten blok się kończy. Python nie ma zasięgu blokowego — ma tylko zasięg funkcji i globalny.

if True:
    temp_token = "ABC"

print(temp_token)   # Działa w Pythonie. Nie skompilowałoby się w Javie.

Nazwa utworzona wewnątrz pętli lub if wycieka do reszty otaczającej funkcji. To założenie, nie błąd — ale nazwa, którą uważałeś za tymczasową, może po cichu zderzyć się z inną później.

Nawyk, który cię ratuje: trzymaj zmienne wewnątrz funkcji (nie luzem na górze pliku) i nadawaj im nazwy na tyle konkretne, by dwie nigdy nie biły się o ten sam identyfikator.

Czytanie kontra ponowne przypisanie globalnych

Funkcja może czytać zmienną globalną, ale domyślnie nie może jej ponownie przypisać — spróbuj, a Python po prostu utworzy nową lokalną o tej samej nazwie:

counter = 0

def increment():
    counter = counter + 1   # 💥 UnboundLocalError — Python widzi lokalne "counter"

Jest słowo kluczowe global, które to wymusza, ale nie sięgaj po nie. Funkcje, które zmieniają stan globalny, są trudne do testowania i ogarnięcia.

Czysty wzorzec to ten, do którego stworzono funkcje: przyjmuj dane przez parametry, oddawaj wyniki przez return.

def increment(counter):
    return counter + 1

counter = increment(counter)   # jawnie, testowalnie, bez niespodzianek

Łączymy to w całość

Dwie funkcje, każda robi jedną rzecz — generuje hasła i je ocenia. Zauważ, jak krótka staje się wykonawcza część na dole, gdy logika żyje w funkcjach.

Utwórz plik security_tool.py:

import random
import string

def generate_password(length=12):
    """Generate a random password of the given length."""
    characters = string.ascii_letters + string.digits + string.punctuation
    password = ""
    for _ in range(length):   # _ = "nie potrzebuję wartości z tej pętli"
        password += random.choice(characters)
    return password

def analyze_strength(password):
    """Rate a password and return a human-readable verdict."""
    if len(password) < 8:
        return "WEAK - Too short. Must be at least 8 characters."
    # any() jest True, jeśli choć jeden element przejdzie test:
    has_number = any(char.isdigit() for char in password)
    has_special = any(char in string.punctuation for char in password)
    if not has_number and not has_special:
        return "WEAK - Missing numbers and special characters."
    elif not has_number or not has_special:
        return "MODERATE - Add both numbers and specials for max security."
    return "STRONG - Meets all security criteria."

# --- GŁÓWNY SKRYPT ---
print("[1] Generating secure password (length 15)...")
print(f"Result: {generate_password(length=15)}\n")

print("[2] Analyzing 'admin':")
print(f"Result: {analyze_strength('admin')}\n")

print("[3] Analyzing 'SuperSecretPass123!':")
print(f"Result: {analyze_strength('SuperSecretPass123!')}")

Każda funkcja return-uje swój werdykt zamiast go wypisywać — co właśnie czyni je wielokrotnego użytku i testowalnymi później. any(...) w parze z wyrażeniem-generatorem to przedsmak wyrażeń listowych, które są zaraz za rogiem.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 12 · Pythonowo

Wyrażenia listowe

⏱ ~26 min 🎯 Średniozaawansowany 4 sekcji + quiz

Powiedz to w jednej linii

🛑 Notka dla programistów: Wyrażenia listowe to LINQ

Wyrażenie listowe to .Where().Select() zwinięte w jedno wyrażenie:

var names = users.Where(u => u.Active).Select(u => u.Name).ToList();  // C#
names = [u.name for u in users if u.active]                          # Python

Programiści Javy: ta sama idea co potok Stream, bez ceremonii.

Kształt jest zawsze taki sam: [wyrażenie for element in iterowalne]:

# Sposób długi:
squares = []
for n in range(1, 6):
    squares.append(n * n)

# Wyrażenie listowe:
squares = [n * n for n in range(1, 6)]   # [1, 4, 9, 16, 25]

Ten sam wynik, jedna linia, bez tymczasowej pustej listy do ogarniania.

Dodaj if, by filtrować

Końcowe if (bez else) zachowuje tylko pożądane elementy — połowa .Where()/.filter():

numbers = [4, 7, 10, 13, 16]
evens = [n for n in numbers if n % 2 == 0]   # [4, 10, 16]

Część wyrażenia (przed for) przekształca każdy element — dowolne wyrażenie: wywołanie metody, matematyka, wycinek:

lines = ["10.0.0.99 POST /login 403", "192.168.1.50 GET / 200"]
ips = [line.split()[0] for line in lines]    # ['10.0.0.99', '192.168.1.50']

W przekształceniu możesz osadzić pythonowy operator warunkowywartość_jeśli_prawda if warunek else wartość_jeśli_fałsz:

codes = [200, 403, 500]
labels = ["OK" if c < 400 else "FAIL" for c in codes]   # ['OK','FAIL','FAIL']

Reguła: końcowe if filtruje; if/else przed for przekształca.

Ta sama idea, nawiasy klamrowe

Zamień [] na {}, a dostaniesz pozostałe dwa.

Wyrażenie zbiorowe buduje zbiór bez duplikatów — idealne do "unikalnych X":

lines = ["192.168.1.50 ... 200", "10.0.0.99 ... 403", "192.168.1.50 ... 404"]
unique_visitors = {line.split()[0] for line in lines}
# {'192.168.1.50', '10.0.0.99'}

Wyrażenie słownikowe używa składni klucz: wartość:

servers = ["web-01", "web-02", "db-01"]
name_lengths = {name: len(name) for name in servers}
# {'web-01': 6, 'web-02': 6, 'db-01': 5}

Możesz nawet filtrować istniejący słownik:

failures = {"a": 1, "b": 4, "c": 2}
suspicious = {ip: n for ip, n in failures.items() if n > 2}   # {'b': 4}

Pythonowo znaczy czytelnie, nie najkrócej

Wyrażenia listowe służą do budowania kolekcji. Sięgnij po zwykłą pętlę, gdy:

  • pętla ma efekty uboczne — wypisywanie, zapis plików, wywołanie API;
  • wyrażenie rośnie na trzy klauzule wgłąb i przestaje mieścić się w jednej linii;
  • potrzebujesz break/continue lub złożonych kroków pośrednich.
# ❌ Nie upychaj efektów ubocznych w wyrażeniu listowym:
[print(x) for x in items]        # działa, ale nadużywa składni

# ✅ Po prostu napisz pętlę:
for x in items:
    print(x)

💡 Wskazówka: Jeśli musisz czytać wyrażenie listowe dwa razy, by je zrozumieć, powinno było być pętlą. Pythonowo znaczy jasno, nie sprytnie.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 13 · Pythonowo

Generatory i lambdy

⏱ ~24 min 🎯 Średniozaawansowany 4 sekcji + quiz

Małe jednorazowe funkcje

Lambda to jednolinijkowa, bezimienna funkcja: lambda argumenty: wyrażenie. Zwraca wyrażenie automatycznie (bez return):

double = lambda x: x * 2
print(double(5))   # 10

Prawie nigdy nie przypiszesz jej do nazwy w ten sposób — jeśli zasługuje na nazwę, użyj def. Lambdy błyszczą jako argument key= dla sorted(), max() i min():

servers = [
    {"name": "web-01", "load": 0.82},
    {"name": "db-01",  "load": 0.31},
    {"name": "web-02", "load": 0.95},
]
busiest_first = sorted(servers, key=lambda s: s["load"], reverse=True)
hottest = max(servers, key=lambda s: s["load"])
print(hottest["name"])   # web-02

Funkcja key mówi wbudowanej funkcji, co porównywać. Bez niej Python nie wiedziałby, które pole słownika masz na myśli.

Wyrażenia, które nie rozsadzą ci RAM-u

Zamień [] na (), a dostaniesz wyrażenie generatorowe. Wygląda niemal identycznie jak wyrażenie listowe, ale produkuje elementy po jednym, na żądanie — zamiast budować całą listę w pamięci naraz:

# Wyrażenie listowe — buduje od razu wszystkie 10 milionów liczb w pamięci:
total = sum([n * n for n in range(10_000_000)])

# Generator — podaje po jednej liczbie do sum(), trzymając ~jedną w pamięci:
total = sum(n * n for n in range(10_000_000))

Oba dają ten sam wynik. Drugi używa maleńkiej, stałej ilości pamięci. To ta sama lekcja co strumieniowanie pliku linijka po linijce — teraz jako cecha języka.

Nic się nie liczy, dopóki nie pociągniesz

🛑 Notka dla programistów: Generatory to leniwe IEnumerable

Generator to pythonowa leniwa sekwencja oparta na yield — koncepcyjnie to samo co C# IEnumerable<T> zbudowane z yield return albo strumień Javy, którego jeszcze nie zakończyłeś. Nic się nie liczy, dopóki coś z niego nie pociągnie (pętla for, sum(), list()).

Wybór między nimi:

  • Użyj wyrażenia listowego, gdy potrzebujesz całej kolekcji pod ręką — będziesz ją indeksować, przejdziesz dwa razy albo sprawdzisz długość.
  • Użyj generatora, gdy skonsumujesz go dokładnie raz, a źródło jest duże — zamieniasz swobodny dostęp na niemal zerową pamięć.

Generatory możesz też pisać przez def + yield:

def countdown(n):
    while n > 0:
        yield n          # zatrzymaj się tu, oddaj n, wznów przy następnym pociągnięciu
        n -= 1

for x in countdown(3):
    print(x)             # 3, 2, 1

Łączymy to w całość

Weź surowe linie logu dostępu i wyprodukuj raport bezpieczeństwa — głównie w jednolinijkach. Używamy collections.Counter, podklasy słownika stworzonej do zliczania.

Utwórz plik log_analyzer.py:

from collections import Counter

raw_lines = [
    "192.168.1.50 GET /index.html 200",
    "10.0.0.99 POST /login 403",
    "192.168.1.50 GET /admin 404",
    "10.0.0.99 POST /login 403",
    "172.16.0.1 GET /index.html 200",
    "10.0.0.99 POST /login 403",
]

# Rozbij każdą linię na (ip, method, path, status) raz, na początku:
records = [tuple(line.split()) for line in raw_lines]

all_ips = [ip for ip, method, path, status in records]
unique_visitors = {ip for ip, method, path, status in records}
failed_ips = [ip for ip, m, p, status in records if status.startswith("4")]

requests_per_ip = Counter(all_ips)
failures_per_ip = Counter(failed_ips)
top_ip, top_count = requests_per_ip.most_common(1)[0]

# Wyrażenie słownikowe filtrujące istniejący słownik:
suspicious = {ip: n for ip, n in failures_per_ip.items() if n > 2}

print("--- Access Log Report ---")
print(f"Total requests:        {len(records)}")
print(f"Unique visitors:       {len(unique_visitors)}")
print(f"Failed requests (4xx): {len(failed_ips)}")
print(f"Top talker: {top_ip} ({top_count} requests)")
print("Suspicious IPs (>2 failures):")
for ip, n in suspicious.items():
    print(f"  - {ip} -> {n} failures")

Policz, ile linii imperatywnego for/if/append zastąpiły wyrażenia listowe. Wersja z wyrażeniami jest nie tylko krótsza — gdy nabierzesz wprawy, każda linia opisuje jedno przekształcenie zamiast przepisu, który wykonujesz w głowie.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 14 · Niezawodność

Obsługa błędów

⏱ ~26 min 🎯 Średniozaawansowany 4 sekcji + quiz

Prawdziwy świat jest wrogi

Do tej pory zakładaliśmy szczęśliwą ścieżkę: użytkownicy się zachowują, pliki istnieją, dane są czyste. Prawdziwy świat taki nie jest. Połączenia padają, dyski się zapełniają, dane przychodzą zniekształcone. Skrypt, który wyrzuca surowy ślad stosu i ginie na pierwszej złej linii, nie jest gotowy do produkcji.

Błędy to nie porażki — to informacja. Mówią ci, że stało się coś nieoczekiwanego, a twój kod decyduje, co z tym zrobić. Python obsługuje je przez try / except, odpowiednik try / catch w C# czy Javie:

try:
    user_id = int("admin")
except ValueError as e:
    print(f"[ERROR] Cannot convert input: {e}")

as e wiąże obiekt wyjątku, żebyś mógł go zbadać lub zalogować.

Żadnego "łapania jak w Pokémonach"

Istnieje antywzorzec na tyle powszechny, że ma nazwę — łapanie jak w Pokémonach, bo "trzeba złapać je wszystkie":

# ❌ NIEBEZPIECZNE: gołe except łapie WSZYSTKO
try:
    result = do_something_risky()
except:
    print("Something went wrong")

Gołe except: jest niebezpiecznie szerokie. Połyka:

  • KeyboardInterrupt — użytkownika naciskającego Ctrl+C, by zatrzymać program
  • SystemExit — celowe sys.exit()
  • literówki w twoim kodzie, które powinny głośno paść podczas pisania

Zawsze nazywaj wyjątek, którego się spodziewasz, a resztę pozwól propagować:

try:
    config = data["timeout"]
except KeyError:
    config = 30   # rozsądna domyślna

TypeError gdzie indziej wciąż się wywali — co podczas pisania jest dokładnie tym, czego chcesz.

Sprzątanie i ścieżka sukcesu

finally uruchamia się bez względu na wszystko — sukces, złapany błąd albo niezłapany błąd wychodzący na zewnątrz. To miejsce na sprzątanie, które musi się stać:

conn = open_connection()
try:
    conn.query("SELECT ...")
except ConnectionError as e:
    print(f"[ERROR] DB unreachable: {e}")
finally:
    conn.close()   # zawsze się uruchamia
    print("[INFO] Connection closed.")

Mniej znany blok else uruchamia się tylko gdy try zakończył się bez wyjątku — przydatny dla kodu, który ma działać po sukcesie, ale nie powinien być "chroniony" przez try:

try:
    value = int(user_input)
except ValueError:
    print("Not a number.")
else:
    print(f"Got a clean number: {value}")   # tylko po sukcesie

Gdy to twój kod wykrywa problem

Użyj raise, by zasygnalizować błąd z własnych funkcji — padnij szybko i jasno, zamiast zwracać fałszywą wartość, która wybuchnie gdzieś daleko:

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("Amount must be positive.")
    if amount > balance:
        raise ValueError("Insufficient funds.")
    return balance - amount

Wywołujący decyduje, jak zareagować:

try:
    balance = withdraw(100, 150)
except ValueError as e:
    print(f"[DENIED] {e}")

💡 Wskazówka: Zgłaszaj najbardziej konkretny wbudowany wyjątek, który pasuje — ValueError dla złej wartości, FileNotFoundError dla brakującego pliku, KeyError dla brakującego klucza. Konkretne wyjątki pozwalają wywołującym obsłużyć każdy przypadek precyzyjnie.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 15 · Niezawodność

Pliki i pathlib

⏱ ~28 min 🎯 Średniozaawansowany 4 sekcji + quiz

open() i instrukcja with

🛑 Notka dla programistów: IDisposable kontra menedżery kontekstu

Znasz ból wyciekniętego uchwytu pliku, gdy ktoś zapomni .Dispose() — dlatego owijasz rzeczy w using. Pythonowy odpowiednik to menedżer kontekstu przez with. Gwarantuje zamknięcie pliku w chwili zakończenia bloku, nawet gdy w środku zostanie rzucony wyjątek.

using (var r = new StreamReader("log.txt")) { ... }   // C#
with open("log.txt") as r:                            # Python
    content = r.read()
# zamknięte automatycznie tutaj
TrybZachowanie
'r'Odczyt (domyślny). FileNotFoundError, jeśli brak.
'w'Zapis — nadpisuje plik; tworzy go, jeśli nie istnieje.
'a'Dopisywanie — dodaje na koniec; tworzy, jeśli nie istnieje.

Pomiń with, a zostawisz wiszące uchwyty — ten sam błąd co zapomniany Dispose().

Czytanie plików oszczędne pamięciowo

Mógłbyś wczytać cały plik do pamięci przez file.read() — ale co, gdy log ma 10 GB? Twój program staje w miejscu. Zamiast tego iteruj wprost po obiekcie pliku, linijka po linijce:

# ✅ Oszczędne pamięciowo — jedna linia w pamięci naraz:
with open("huge_log.txt", "r") as file:
    for line in file:
        process(line)

To czyta linię, przetwarza ją, odrzuca, bierze następną. Plik 10 GB przepływa tak gładko jak plik 10 KB. To ta sama lekcja co generatory w rozdziale 4 — nie trzymaj w pamięci więcej, niż potrzebujesz.

Zapis jest równie prosty:

with open("audit_log.txt", "a") as file:
    file.write("User 'admin' logged in\n")   # \n — write() nie dodaje znaków nowej linii

Przestań sklejać ścieżki ze stringów

Wpisanie na sztywno / lub \\ to błąd czekający, by wystrzelić na innym systemie. pathlib (Python 3.4+) to nowoczesny standard. Przeciąża /, by łączyć ścieżki, i emituje właściwy separator dla bieżącego systemu:

from pathlib import Path

# Działa na Linuksie, macOS i Windowsie:
log_path = Path.home() / "server_logs" / "access.log"

if not log_path.exists():
    print("Log file missing!")

Kilka metod, których będziesz używać bez przerwy:

Metoda / właściwośćCel
path.exists() / .is_file() / .is_dir()sprawdzenia istnienia
path.read_text() / path.write_text(s)jednorazowy odczyt/zapis
path.parentkatalog nadrzędny
path.name / .stem / .suffixaccess.log / access / .log

Łączymy to w całość

Solidne narzędzie: wczytaj częściowo uszkodzony log, wyciągnij poprawne IP, pomiń złe linie bez wywałki i strumieniuj czyste wyjście do nowego pliku. Prawdziwą lekcją jest architektura — zagnieżdżone try/except na dwóch poziomach.

Utwórz plik log_cleanser.py:

import re
from pathlib import Path

# Zgrubny wzorzec IP: cztery grupy 1-3 cyfr, oddzielone kropkami.
IP_PATTERN = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")

input_path = Path("raw_traffic.log")
output_path = Path("clean_ips.txt")
valid_ips, skipped, total = [], 0, 0

print("[INFO] Processing log file...")
try:
    # with zarządza OBOMA plikami; oba zamkną się czysto nawet przy błędzie.
    with open(input_path, "r") as infile, open(output_path, "w") as outfile:
        for line_number, line in enumerate(infile, start=1):
            total += 1
            try:                                  # WEWNĘTRZNE try: jedna zła linia nie zabije całości
                match = IP_PATTERN.search(line.strip())
                if match:
                    ip = match.group()
                    valid_ips.append(ip)
                    outfile.write(ip + "\n")
                else:
                    skipped += 1
                    print(f'[WARN] Skipping line {line_number}: "{line.strip()[:40]}"')
            except (ValueError, AttributeError):
                skipped += 1
except FileNotFoundError:                          # ZEWNĘTRZNE except: katastrofalna awaria
    print(f"[ERROR] File not found: {input_path}")
else:                                              # uruchamia się tylko gdy plik przetworzono w całości
    print(f"[SUCCESS] Extracted {len(valid_ips)} IPs from {total} lines.")
    print(f"[SUCCESS] Saved to {output_path}")

Spójrz na architekturę: zewnętrzne try łapie "pliku w ogóle nie ma"; wewnętrzne try łapie "ta jedna linia jest zepsuta"; else uruchamia się tylko przy pełnym sukcesie. To warstwowanie odróżnia kod produkcyjny od skryptu-zabawki — łagodnie się degraduje i działa dalej.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 16 · Niezawodność

Moduły, pakiety i biblioteka standardowa

⏱ ~24 min 🎯 Średniozaawansowany 4 sekcji + quiz

Ponowne użycie tego, co napisali inni (i ty)

Moduł to po prostu plik .py. import wnosi jego zawartość do twojego programu. Biblioteka standardowa to ogromny zestaw modułów dostarczanych z Pythonem — "baterie w zestawie".

import random                  # cały moduł; używaj jak random.randint(...)
from pathlib import Path       # wyciągnij jedną nazwę; używaj jak Path(...)
from collections import Counter, defaultdict   # kilka nazw
import statistics as stats     # alias dla długiej nazwy

Formy do zapamiętania:

  • import module — dostęp z przestrzenią nazw (module.thing). Najbezpieczniej; bez konfliktów nazw.
  • from module import name — wciągnij nazwę bezpośrednio. Wygodnie.
  • import module as alias — skróć długą nazwę (np. import numpy as np).

⚠️ Uwaga: Unikaj from module import *. Zrzuca każdą nazwę do twojego pliku, ukrywając skąd rzeczy pochodzą i zapraszając ciche konflikty.

Przegląd biblioteki standardowej

Kilku już użyłeś. Parę wartych poznania, zanim w ogóle sięgniesz po pakiet zewnętrzny:

ModułDo czego
randomliczby i wybory losowe
mathsqrt, pi, floor, ceil, …
datetimedaty, czasy, formatowanie
pathlibścieżki w systemie plików
jsonodczyt/zapis JSON
rewyrażenia regularne
collectionsCounter, defaultdict, deque
itertoolsklocki do budowy iteratorów
os / sysśrodowisko i interpreter
import json
data = {"name": "ada", "level": 5}
text = json.dumps(data)        # dict -> string JSON
back = json.loads(text)        # string JSON -> dict

💡 Wskazówka: Zanim zainstalujesz pakiet lub samodzielnie napiszesz narzędzie, sprawdź bibliotekę standardową — odpowiedź często już tam jest.

Dzielenie projektu na pliki

Gdy program rośnie, podziel go na moduły. Umieść pomocniki w utils.py, zaimportuj je z main.py:

# utils.py
def greet(name):
    return f"Hello, {name}!"
# main.py
from utils import greet
print(greet("Ada"))

Pakiet to katalog modułów. Import uruchamia moduł od góry do dołu raz, potem go buforuje — zaimportuj ten sam moduł dziesięć razy, jego kod najwyższego poziomu uruchomi się raz.

💡 Wskazówka: Trzymaj moduły skupione — każdy z jedną jasną odpowiedzialnością. "Plik luźno powiązanych rzeczy" to zapach; "plik o parsowaniu logów" to moduł.

Idiom w każdym skrypcie Pythona

Zobaczysz to na dole niemal każdego skryptu:

def main():
    print("Running the tool...")

if __name__ == "__main__":
    main()

Znaczy to "uruchom main() tylko, gdy ten plik jest wykonywany bezpośrednio, a nie gdy jest importowany przez inny plik". Gdy uruchamiasz python tool.py, Python ustawia specjalną zmienną __name__ na "__main__". Gdy inny plik robi import tool, __name__ to zamiast tego "tool" — więc strzeżony kod milczy.

Dlaczego to ważne: pozwala plikowi być zarazem uruchamialnym programem i importowalną biblioteką funkcji. To rozróżnienie staje się kluczowe w chwili, gdy piszesz testy (rozdział 7), które import-ują twój kod, by sprawdzić jego funkcje bez odpalania całego skryptu.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 17 · OOP

Klasy i obiekty

⏱ ~28 min 🎯 Średniozaawansowany 4 sekcji + quiz

Łączenie danych i zachowania

Każdy dotychczasowy program trzymał dane luzem — IP tu, licznik tam. Klasa pozwala spakować dane oraz zachowanie, które na nich operuje, w jeden czysty typ. Klasa to plan; z niej tworzysz instancje (obiekty).

class Server:
    def __init__(self, name, ip, cpu_load):
        self.name = name
        self.ip = ip
        self.cpu_load = cpu_load

web1 = Server("web-01", "192.168.1.10", 0.45)
print(web1.name)      # web-01
print(web1.cpu_load)  # 0.45
  • __init__ to konstruktor — uruchamia się automatycznie, gdy tworzysz instancję. Podwójne podkreślenia oznaczają metodę "dunder" (double-underscore).
  • self to instancja budowana lub na której operujemy. self.name = name zapisuje wartość na tej instancji.

Zachowanie, które żyje z danymi

Funkcja zdefiniowana wewnątrz klasy to metoda. Czyta i modyfikuje instancję przez self:

class Server:
    def __init__(self, name, cpu_load):
        self.name = name
        self.cpu_load = cpu_load

    def is_overloaded(self, threshold=0.85):
        """Return True if this server's load is above the threshold."""
        return self.cpu_load > threshold

web2 = Server("web-02", 0.91)
print(web2.is_overloaded())   # True

🛑 Notka dla programistów: Gdzie jest this? Czemu self jest jawne?

W C#/Javie this jest niejawne. W Pythonie instancja przekazywana jest jawnie jako pierwszy parametr każdej metody, z konwencji nazwany self. Piszesz def is_overloaded(self): i wołasz web2.is_overloaded() — Python podpina ci web2 pod self. (A self.name to pole; gołe name byłoby zmienną lokalną, która znika, gdy metoda kończy działanie — reguła zasięgu z rozdziału 4.)

Dlaczego zwykłe klasy są niezgrabne

Zobacz, co się dzieje, gdy wypiszesz lub porównasz instancje zwykłej klasy:

web1 = Server("web-01", 0.45)
print(web1)
# <__main__.Server object at 0x7f3c1a2b8d90>   ← bezużyteczne

a = Server("web-01", 0.45)
b = Server("web-01", 0.45)
print(a == b)   # False  ← a są identyczne!

Domyślnie wypisanie pokazuje adres w pamięci, a == sprawdza, czy dwie zmienne wskazują na ten sam obiekt, a nie czy ich zawartość się zgadza. Żeby naprawić oba, musiałbyś ręcznie napisać kolejne dwie metody dunder — __repr__ dla czytelnego wypisu i __eq__ dla porównania po wartości — przepisując nazwę każdego pola. Dla klasy z ośmioma polami to dużo powtarzalnego, podatnego na błędy pisania.

Jest lepszy sposób i to cała istota następnej lekcji: dataclasses.

Każdy obiekt jest niezależny

Każda instancja niesie własną kopię danych. Metody mogą zmieniać ten stan w czasie — to właśnie sprawia, że obiekty wydają się żywe:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1

a = Counter()
b = Counter()
a.increment()
a.increment()
print(a.count, b.count)   # 2 0  — niezależny stan

a i b to osobne obiekty; podbicie a nie rusza b. To podstawowa zmiana myślenia w OOP: przestajesz przekazywać luźne wartości, a zaczynasz prosić obiekty, by zarządzały własnymi danymi.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 18 · OOP

Dataclasses

⏱ ~24 min 🎯 Średniozaawansowany 4 sekcji + quiz

Zadeklaruj pola raz, resztę dostań za darmo

Biblioteka standardowa Pythona naprawia boilerplate dekoratorem @dataclass. Zadeklaruj pola raz z adnotacjami typów, a Python wygeneruje za ciebie __init__, __repr__ i __eq__:

from dataclasses import dataclass

@dataclass
class Server:
    name: str
    ip: str
    cpu_load: float

    def is_overloaded(self, threshold=0.85):
        return self.cpu_load > threshold

To cała klasa — bez __init__, bez kopiowania self.name = name. Teraz wszystko po prostu działa:

a = Server("web-01", "192.168.1.10", 0.45)
b = Server("web-01", "192.168.1.10", 0.45)
print(a)        # Server(name='web-01', ip='192.168.1.10', cpu_load=0.45)
print(a == b)   # True  — porównane po wartości

Linia @dataclass to dekorator — funkcja, która owija twoją klasę i dodaje jej możliwości. Na razie wiedz tylko, że wykonuje za ciebie żmudne pisanie.

Znana idea, mniej ceremonii

🛑 Notka dla programistów: Dataclasses ≈ rekordy / struktury

@dataclass to pythonowa odpowiedź na C# record lub Java record (i bliska strukturze dla zwykłych danych). Ten sam cel: zadeklaruj pola, dostań równość po wartości i sensowne ToString()/__repr__ za darmo, pomiń boilerplate konstruktora.

Jedna różnica: domyślny @dataclass jest zmienny — możesz przypisać a.cpu_load = 0.9. Chcesz niezmienności rekordu? Użyj @dataclass(frozen=True), a ponowne przypisanie zgłosi błąd — przydatne dla wartości, które nigdy nie powinny się zmienić po utworzeniu.

Dataclasses to właściwy domyślny wybór dla "worka powiązanych danych z paroma metodami". Sięgaj po nie bez przerwy.

Domyślne pól i jedna, której nie wolno pisać

Pola dataclass mogą mieć wartości domyślne (które muszą być po polach bez domyślnych, ta sama reguła co w funkcjach):

from dataclasses import dataclass, field

@dataclass
class Server:
    name: str
    cpu_load: float = 0.0                       # domyślnie bezczynny
    tags: list = field(default_factory=list)    # patrz notka poniżej

⚠️ Uwaga: Nigdy nie pisz tags: list = []. Wartość domyślna jest tworzona raz i współdzielona przez wszystkie instancje — współdzielona [] oznacza, że każdy serwer po cichu wskazuje na tę samą listę; dopisz do jednej, a zmienią się wszystkie. field(default_factory=list) buduje świeżą pustą listę dla każdej instancji.

(Programiści C#/Java: to klasyczna pułapka "współdzielonej zmiennej wartości domyślnej", a Python zmusza cię, byś świadomie z niej zrezygnował.)

Łączymy to w całość

Zamodeluj małą flotę i wyprodukuj raport statusu. Zauważ, jak wyrażenia listowe z rozdziału 4 wracają do filtrowania — obiekty i wyrażenia listowe pięknie się parują.

Utwórz plik fleet_monitor.py:

from dataclasses import dataclass

LOAD_THRESHOLD = 0.85

@dataclass
class Server:
    name: str
    ip: str
    cpu_load: float

    def is_overloaded(self):
        return self.cpu_load > LOAD_THRESHOLD

    def status_line(self):
        """A single formatted row for the report."""
        icon = "⚠️ " if self.is_overloaded() else "✅"
        label = "overloaded" if self.is_overloaded() else "healthy"
        # :<8 dopełnia nazwę z lewej; :<14 IP; :.2f trzyma obciążenie schludnie.
        return f"{self.name:<8} {self.ip:<14} load {self.cpu_load:.2f}   {icon} {label}"

def main():
    fleet = [
        Server("web-01", "192.168.1.10", 0.45),
        Server("web-02", "192.168.1.11", 0.91),
        Server("db-01",  "192.168.1.20", 0.30),
    ]

    print("--- Fleet Status ---")
    for server in fleet:
        print(server.status_line())

    overloaded = [s for s in fleet if s.is_overloaded()]   # wyrażenie listowe po obiektach
    print()
    if overloaded:
        names = ", ".join(s.name for s in overloaded)
        print(f"[ALERT] {len(overloaded)} server over threshold: {names}")
    else:
        print("[OK] All servers within healthy limits.")

if __name__ == "__main__":
    main()

Zwróć uwagę na dwie rzeczy: ", ".join(...) zszywa nazwy w listę rozdzieloną przecinkami, a if __name__ == "__main__": (rozdział 5) sprawia, że main() działa przy bezpośrednim uruchomieniu, ale nie przy imporcie — co właśnie pozwoli nam przetestować ten plik dalej.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 19 · Ekosystem

venv, pip i API na żywo

⏱ ~30 min 🎯 Średniozaawansowany 5 sekcji + quiz

Izolacja per projekt

Dwa projekty na jednej maszynie: A potrzebuje wersji 1.0 biblioteki, B wersji 2.0. Zainstaluj globalnie, a się zderzą — "piekło zależności". Wirtualne środowisko to izolowany Python per projekt z własnymi prywatnymi pakietami.

🛑 Notka dla programistów: venv + pip ≈ twój menedżer pakietów

pip to instalator Pythona — zgrubny odpowiednik NuGet, Maven czy npm. requirements.txt to twoja lista <PackageReference> z .csproj / package.json. Różnica: izolacja jest oparta na katalogu i aktywowana per powłoka.

python -m venv .venv              # utwórz (venv jest częścią Pythona)

source .venv/bin/activate         # aktywuj — macOS / Linux
# .venv\Scripts\Activate.ps1      # aktywuj — Windows PowerShell

Twój znak zachęty pokazuje (.venv). Teraz python i pip wskazują na izolowane środowisko. Wpisz deactivate, by wyjść. Dodaj .venv/ do .gitignore — commitujesz listę zależności, nie pakiety.

pip i powtarzalne instalacje

Z aktywnym środowiskiem zainstaluj requests — de facto standardową bibliotekę HTTP, przyjaźniejszą niż wbudowany urllib:

pip install requests

Zamroź zależności, by projekt był powtarzalny:

pip freeze > requirements.txt

To zapisuje każdy zainstalowany pakiet i jego dokładną wersję. Każdy (w tym przyszły ty) odtwarza środowisko jednym poleceniem:

pip install -r requirements.txt

💡 Wskazówka: Commituj requirements.txt, nigdy folder .venv/. Lista odbudowuje środowisko wszędzie.

Rozmowa z REST API

requests czyni GET jednolinijką. Owiń ją w obsługę błędów z rozdziału 5 — sieć to dokładnie wrogie, zawodne miejsce, do którego stworzono try/except:

import requests

try:
    response = requests.get("https://api.github.com/users/octocat", timeout=10)
    response.raise_for_status()   # zamień 4xx/5xx w wyjątek
    data = response.json()        # sparsuj ciało JSON do słownika Pythona
    print(data["name"])           # The Octocat
except requests.exceptions.RequestException as e:
    print(f"[ERROR] Request failed: {e}")

Trzy rzeczy warte podkreślenia:

  • timeout=10zawsze ustawiaj. Bez tego zawieszony serwer zamrozi twój skrypt na zawsze. Brak timeoutu to jeden z najczęstszych błędów produkcyjnych.
  • raise_for_status() — domyślnie requests nie rzuca na 404/500; to zamienia zły status w wyjątek, który twój except może obsłużyć.
  • response.json() — parsuje ciało JSON wprost do słowników i list.

Nigdy nie wpisuj klucza na sztywno

Wiele API potrzebuje tokenu. Reguła, która się liczy: nigdy nie wpisuj sekretu na sztywno w kodzie. Klucz wklejony do pliku .py trafia do gita, zostaje wypchnięty i zeskanowany przez boty w ciągu minut. Standardowy dom sekretów to zmienna środowiskowa:

import os
token = os.environ.get("GITHUB_TOKEN")   # wartość lub None, jeśli nieustawiona — bez wywałki
if token:
    print("Token found.")

Ustaw ją w powłoce przed uruchomieniem:

export GITHUB_TOKEN="ghp_your_token_here"          # macOS / Linux
# $env:GITHUB_TOKEN = "ghp_..."                    # Windows PowerShell

Ponieważ sekret żyje w środowisku, nie w pliku, nigdy nie trafia do gita. (W lokalnym developmencie wielu używa ignorowanego przez gita pliku .env z pakietem python-dotenv — ta sama zasada.) Dokładnie tak obsłużymy klucz AI w projekcie finałowym.

Łączymy to w całość

Skrypt, który pobiera publiczne repozytoria użytkownika i raportuje o nich — requests, obsługa błędów, opcjonalny token oraz wyrażenia listowe i sortowanie key= z wcześniej.

Utwórz plik repo_inspector.py (aktywne venv, zainstalowany requests):

import os
import requests

API_BASE = "https://api.github.com"

def build_headers():
    headers = {"Accept": "application/vnd.github+json"}
    token = os.environ.get("GITHUB_TOKEN")
    if token:
        headers["Authorization"] = f"Bearer {token}"
    return headers

def fetch_repos(username):
    url = f"{API_BASE}/users/{username}/repos"
    params = {"per_page": 100, "sort": "updated"}
    response = requests.get(url, headers=build_headers(), params=params, timeout=10)
    response.raise_for_status()
    return response.json()

def main():
    username = "octocat"
    print(f"--- GitHub Repo Inspector: {username} ---")
    try:
        repos = fetch_repos(username)
    except requests.exceptions.HTTPError as e:
        code = e.response.status_code
        msg = {404: "No such user.", 403: "Rate limited — set GITHUB_TOKEN."}.get(code, f"GitHub returned {code}.")
        print(f"[ERROR] {msg}")
        return
    except requests.exceptions.RequestException as e:
        print(f"[ERROR] Could not reach GitHub: {e}")
        return

    own = [r for r in repos if not r["fork"]]                 # wyrażenie listowe
    total_stars = sum(r["stargazers_count"] for r in own)     # generator
    top = sorted(own, key=lambda r: r["stargazers_count"], reverse=True)[:3]

    print(f"Found {len(own)} non-fork repos, {total_stars} total stars.\n")
    for rank, repo in enumerate(top, start=1):
        lang = repo["language"] or "no language"             # None lub wartość zastępcza (truthy)
        print(f"  {rank}. {repo['name']:<20} ⭐ {repo['stargazers_count']:<6} ({lang})")

if __name__ == "__main__":
    main()

Pobierasz teraz dane na żywo z internetu i raportujesz o nich — z eleganckim obsłużeniem brakującego użytkownika, limitu zapytań i martwego połączenia. To repo["language"] or "no language" opiera się na regule truthy/falsy z rozdziału 2.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 20 · Jakość

Adnotacje typów i mypy

⏱ ~26 min 🎯 Zaawansowany 4 sekcji + quiz

Opcjonalna, sprawdzalna dokumentacja

Python jest typowany dynamicznie — nigdy nie musisz deklarować typów. Ale od wersji 3.5 możesz, opcjonalnie, je adnotować. Te adnotacje to podpowiedzi typów (type hints):

def greet(name: str) -> str:
    return f"Hello, {name}"

attempts: int = 0
ratio: float = 0.85

Czytaj name: str jako "oczekuje się, że name będzie stringiem", a -> str jako "ta funkcja zwraca string". Dla kolekcji i wartości opcjonalnych nowoczesna składnia (3.10+) jest czysta:

def total(numbers: list[int]) -> int:
    return sum(numbers)

# str | None znaczy "string albo None" — wartość może być nieobecna:
def find_language(repo: dict) -> str | None:
    return repo.get("language")

Podpowiedzi się nie wykonują

🛑 Notka dla programistów: Typowanie stopniowe — podpowiedzi się nie wykonują

W C#/Javie typy wymusza kompilator; kod je łamiący się nie zbuduje. Podpowiedzi Pythona nie są wymuszane w czasie działania — interpreter całkowicie je ignoruje. To uruchamia się bez słowa skargi:

def total(numbers: list[int]) -> int:
    return sum(numbers)

total("not a list at all")   # Python to uruchamia — podpowiedź to tylko notka

To typowanie stopniowe: dodajesz podpowiedzi tam, gdzie pomagają, pomijasz tam, gdzie nie, mieszasz swobodnie. Podpowiedzi stają się przydatne na dwa sposoby — twój edytor używa ich do autouzupełniania i ostrzeżeń, a osobne narzędzie mypy czyta je, by wyłapać niezgodności zanim uruchomisz. Traktuj podpowiedzi jak sprawdzalną dokumentację: komentarze, które narzędzie może zweryfikować, że nigdy się nie zdezaktualizowały.

Statyczny kontroler typów

mypy czyta twoje podpowiedzi i zgłasza kod, który im przeczy — bez wykonywania. Zainstaluj go w wirtualnym środowisku:

pip install mypy

Wskaż mu plik:

mypy repo_stats.py

Gdybyś napisał total("not a list"), mypy powie ci to zanim w ogóle uruchomisz:

error: Argument 1 to "total" has incompatible type "str"; expected "list[int]"

To cała klasa błędów — "przekazałem nie to" — złapana przy biurku zamiast na produkcji. Czyste przejście wypisuje:

Success: no issues found in 1 source file

💡 Wskazówka: Nie musisz otypować wszystkiego naraz. Dodaj podpowiedzi najpierw do funkcji publicznych — tam korzyść jest największa.

Korzyść

Jeśli podpowiedzi się nie wykonują, po co je dodawać? Trzy konkretne wygrane:

  • Supermoce edytora. Autouzupełnianie wie, czym jest wartość, więc podpowiada właściwe metody i flaguje literówki w trakcie pisania.
  • Pewność przy refaktoryzacji. Zmień sygnaturę funkcji, a mypy natychmiast pokaże każde miejsce wywołania, które już nie pasuje.
  • Żywa dokumentacja. def fetch(url: str) -> dict: mówi następnemu czytelnikowi dokładnie, co wchodzi i co wychodzi, a kontroler gwarantuje, że dokument nigdy nie odpłynie od rzeczywistości.

Złoty środek: otypuj granice funkcji (parametry i wartości zwracane) oraz struktury danych płynące między modułami. Zostaw przelotne zmienne lokalne w spokoju. To dyscyplina, która zamienia skrypt w oprogramowanie — i przygotowuje testy z następnej lekcji, bo otypowane, czyste funkcje to najłatwiejsze do testowania rzeczy na świecie.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 21 · Jakość

Testowanie z pytest

⏱ ~28 min 🎯 Zaawansowany 5 sekcji + quiz

Skrypt kontra oprogramowanie

Istnieje granica, którą każdy projekt w końcu przekracza — ta oddzielająca skrypt (działa na mojej maszynie, dziś, jak zmrużę oczy) od oprogramowania (działa niezawodnie, przeżywa refaktoryzacje, da się zmieniać bez strachu). Automatyczne testy cię przez nią przeprowadzają.

Wyobraź sobie skrypt GitHuba z rozdziału 6. Skąd wiedziałbyś, że refaktoryzacja po cichu zepsuła logikę liczenia gwiazdek? Uruchomiłbyś go, obejrzał wynik i miał nadzieję. To się nie skaluje. Testy zamieniają "myślę, że działa" w "potrafię udowodnić, że działa, w 0,03 sekundy".

Kluczowa technika — i to, co w ogóle czyni skrypt dotykający sieci testowalnym — to oddzielenie czystej logiki od efektów ubocznych.

Czyste funkcje

Problem z testowaniem funkcji wołającej API na żywo: test zależny od sieci jest wolny, kapryśny i pada, gdy GitHub ma gorszy dzień — to testowanie GitHuba, nie twojego kodu.

Rozwiązaniem jest zasada projektowa: oddziel część rozmawiającą ze światem od części, która myśli. Funkcja I/O (wywołanie HTTP) zostaje cienka. Funkcje liczące rzeczy powinny być czyste — to samo wejście, to samo wyjście, bez sieci, bez plików, bez niespodzianek:

from dataclasses import dataclass

@dataclass
class Repo:
    name: str
    stars: int
    is_fork: bool

def non_forks(repos: list[Repo]) -> list[Repo]:
    return [r for r in repos if not r.is_fork]

def total_stars(repos: list[Repo]) -> int:
    return sum(r.stars for r in repos)

Czyste funkcje są trywialne do testowania — wręczasz im dane i sprawdzasz, co wraca. (To też powód, dla którego rozdział 4 promował return nad print.)

Konwencje pytest

pytest to narzędzie, na którym ustandaryzowała się społeczność. Zainstaluj je, a potem trzymaj się trzech orzeźwiająco minimalnych konwencji:

pip install pytest
  • Umieść testy w plikach nazwanych test_*.py.
  • Napisz każdy test jako funkcję nazwaną test_*.
  • Użyj zwykłego assert do sprawdzania oczekiwań — żadnych specjalnych metod do zapamiętania.
from repo_stats import Repo, non_forks, total_stars

def test_non_forks_filters_out_forks():
    repos = [Repo("a", 10, False), Repo("b", 5, True), Repo("c", 8, False)]
    result = non_forks(repos)
    assert len(result) == 2
    assert all(not r.is_fork for r in result)

def test_total_stars_sums_correctly():
    assert total_stars([Repo("a", 10, False), Repo("b", 5, False)]) == 15

def test_total_stars_of_empty_list_is_zero():
    assert total_stars([]) == 0   # przypadki brzegowe to miejsce, gdzie kryją się błędy
pytest -q
...                                                        [100%]
3 passed in 0.02s

Każda zielona kropka to przechodzący test. Zepsuj kod celowo, a pytest głośno padnie, mówiąc dokładnie, który assert pękł i czego oczekiwał.

parametrize

Gdy chcesz tę samą logikę testu na kilku wejściach, kopiowanie funkcji testowych jest marnotrawstwem. Dekorator parametrize z pytest uruchamia jeden test raz na każdy wiersz danych:

import pytest
from repo_stats import Repo, total_stars

@pytest.mark.parametrize("stars, expected", [
    ([], 0),
    ([10], 10),
    ([10, 20, 30], 60),
])
def test_total_stars_cases(stars, expected):
    repos = [Repo(f"repo{i}", s, False) for i, s in enumerate(stars)]
    assert total_stars(repos) == expected

Trzy odrębne przypadki — pusty, pojedynczy, kilka — z jednego ciała funkcji. pytest raportuje każdy wiersz osobno, więc awaria wskazuje wprost na wejście, które pękło.

Korzyść: zielone znaczy jedź

Gdy mypy jest zielone i pytest jest zielone, możesz refaktoryzować bez strachu — narzędzia powiedzą ci w chwili, gdy coś się obsunie.

Kilka nawyków, które się kumulują:

  • Testuj brzegi. Puste wejście, zero, jeden element, maksimum — tam kryją się błędy.
  • Testuj zachowanie, nie implementację. Asercjuj co funkcja zwraca, a nie jak to liczy, żeby testy przeżyły refaktoryzacje.
  • Trzymaj I/O na brzegach. Wywołanie sieciowe żyje w jednej cienkiej funkcji; wszystko, co karmi, jest czyste i pokryte.

To "bezpieczeństwo typów + testy jednostkowe na twoim skrypcie API", które obiecał kurs: część dotykająca sieci zostaje cienka, logika jest czysta, otypowana i pokryta. Ta natychmiastowa, godna zaufania informacja zwrotna to cała istota testowania — i fundament pod projekt finałowy, który zaraz zbudujesz.

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

Lekcja 22 · Projekt

Projekt finałowy: narzędzie CLI z AI

⏱ ~40 min 🎯 Zaawansowany 6 sekcji + quiz

Ten, do którego zmierzaliśmy

Przez cały kurs zebrałeś każdy potrzebny element; teraz złożymy je w narzędzie, które naprawdę chciałbyś mieć pod ręką: ai_tool — program wiersza poleceń, który łączy się z API AI i albo streszcza tekst, albo recenzuje kod. Wskaż mu plik, powiedz, co ma zrobić, a wystrumieniuje odpowiedź.

Zobacz, jak wiele z kursu to wciąga:

  • wirtualne środowisko i zewnętrzny SDK zainstalowany przez pip (Rozdz. 6)
  • klucz API trzymany bezpiecznie w zmiennej środowiskowej (Rozdz. 6)
  • funkcje o pojedynczych, jasnych zadaniach (Rozdz. 4)
  • try/except wokół wszystkiego, co może paść (Rozdz. 5)
  • pathlib do czytania pliku wejściowego (Rozdz. 5)
  • adnotacje typów, by kod dokumentował się sam (Rozdz. 7)
  • słownik MODES sterujący zachowaniem (Rozdz. 3)
  • plus jedno nowe narzędzie z biblioteki standardowej: argparse, do argumentów wiersza poleceń

argparse: kształt CLI

Dobre narzędzie wiersza poleceń czyta instrukcje z linii poleceń, a nie z wartości wpisanych na sztywno. Biblioteka standardowa dostarcza argparse — parsuje argumenty, waliduje je i generuje ekran --help za darmo:

import argparse

parser = argparse.ArgumentParser(description="Summarize text or review code with AI.")
parser.add_argument("mode", choices=["summarize", "review"], help="What to do.")
parser.add_argument("file", help="Path to the input file, or - for stdin.")
args = parser.parse_args()

print(args.mode, args.file)

Uruchom python ai_tool.py --help, a argparse wypisze użycie automatycznie. Podaj zły tryb, a choices= odrzuci go z jasnym komunikatem — walidacja, którą inaczej musiałbyś sklecić ręcznie.

Plik lub stdin, jedna otypowana funkcja

Przyjmujemy ścieżkę pliku albo wejście z potoku (żeby cat notes.txt | python ai_tool.py summarize - działało). Jedna mała, otypowana funkcja obsługuje oba, używając pathlib z rozdziału 5:

import sys
from pathlib import Path

def read_input(source: str) -> str:
    """Read text from a file path, or from stdin if source is '-'."""
    if source == "-":
        return sys.stdin.read()
    path = Path(source)
    if not path.is_file():
        raise FileNotFoundError(f"No such file: {source}")
    return path.read_text(encoding="utf-8")

Zwraca string albo zgłasza FileNotFoundError — a ponieważ to mała, niemal czysta funkcja, mógłbyś ją przetestować dokładnie jak te z rozdziału 7.

SDK, klucz i strumieniowanie

Zainstaluj oficjalny SDK Anthropic (aktywne venv), a potem umieść klucz w środowisku — nigdy w kodzie:

pip install anthropic
pip freeze > requirements.txt
export ANTHROPIC_API_KEY="sk-ant-..."     # Windows: $env:ANTHROPIC_API_KEY = "..."

Żądanie to jedno wywołanie metody: który model, rozmowa (lista wiadomości) i opcjonalny prompt system, ustawiający rolę modelu. Dla CLI strumieniowanie wypisuje odpowiedź token po tokenie — SDK daje ci menedżer kontekstu, zupełnie jak obsługa plików:

import anthropic

client = anthropic.Anthropic()   # czyta ANTHROPIC_API_KEY ze środowiska

with client.messages.stream(
    model="claude-opus-4-8",
    max_tokens=1024,
    system="You are a precise summarizer.",
    messages=[{"role": "user", "content": text}],
) as stream:
    for chunk in stream.text_stream:
        print(chunk, end="", flush=True)   # flush wypycha każdy fragment natychmiast

🛑 Notka dla programistów: Model to (w większości) bezstanowa funkcja

Myśl o API jak o wywołaniu funkcji, nie o czacie, który cię pamięta. Każde żądanie jest niezależne — model nic nie zachowuje między wywołaniami; by dać kontynuacji kontekst, przesyłasz całą rozmowę ponownie. Dwa kruczki wobec czystych funkcji z rozdziału 7: ten sam prompt może dać nieco inne sformułowanie za każdym razem, a płacisz za token w obie strony. Prompt system to twoja gałka konfiguracji — zmieniasz zachowanie, edytując string, a nie przepisując logikę.

Łączymy wszystko w całość

Zapisz jako ai_tool.py. Rozpoznasz każdą technikę z poprzednich lekcji:

import argparse
import os
import sys
from pathlib import Path

import anthropic

MODEL = "claude-opus-4-8"

# Słownik mapuje każdy tryb na jego prompt system i limit odpowiedzi.
# Dodanie trybu później to jeden nowy wpis tutaj — bez ruszania logiki (Rozdz. 3).
MODES = {
    "summarize": {
        "system": ("You are a precise summarizer. Produce a one-sentence TL;DR, then "
                   "3-5 bullet points. Use only information present in the text."),
        "max_tokens": 1024,
    },
    "review": {
        "system": ("You are a constructive code reviewer. List concrete issues ordered "
                   "by severity — bugs and security first, then clarity. Reference "
                   "specific lines. If the code is solid, say so plainly."),
        "max_tokens": 4096,
    },
}

def read_input(source: str) -> str:
    """Read text from a file path, or from stdin if source is '-'."""
    if source == "-":
        return sys.stdin.read()
    path = Path(source)
    if not path.is_file():
        raise FileNotFoundError(f"No such file: {source}")
    return path.read_text(encoding="utf-8")

def run(mode: str, text: str) -> None:
    """Send the text to Claude in the given mode and stream the reply."""
    config = MODES[mode]
    client = anthropic.Anthropic()
    with client.messages.stream(
        model=MODEL,
        max_tokens=config["max_tokens"],
        system=config["system"],
        messages=[{"role": "user", "content": text}],
    ) as stream:
        for chunk in stream.text_stream:
            print(chunk, end="", flush=True)
    print()

def main() -> None:
    parser = argparse.ArgumentParser(description="Summarize text or review code using Claude.")
    parser.add_argument("mode", choices=MODES.keys(), help="What to do with the input.")
    parser.add_argument("file", help="Path to the input file, or - to read stdin.")
    args = parser.parse_args()

    if not os.environ.get("ANTHROPIC_API_KEY"):
        print("[ERROR] ANTHROPIC_API_KEY is not set.", file=sys.stderr)
        sys.exit(1)

    try:
        text = read_input(args.file)
    except (FileNotFoundError, OSError) as e:
        print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)
    if not text.strip():
        print("[ERROR] The input is empty — nothing to do.", file=sys.stderr)
        sys.exit(1)

    label = "stdin" if args.file == "-" else args.file
    print(f"--- {args.mode.title()} {label} ---\n")

    try:
        run(args.mode, text)
    except anthropic.AuthenticationError:
        print("\n[ERROR] Invalid API key.", file=sys.stderr); sys.exit(1)
    except anthropic.RateLimitError:
        print("\n[ERROR] Rate limited. Try again shortly.", file=sys.stderr); sys.exit(1)
    except anthropic.APIError as e:
        print(f"\n[ERROR] The API call failed: {e}", file=sys.stderr); sys.exit(1)

if __name__ == "__main__":
    main()
python ai_tool.py summarize article.txt
python ai_tool.py review calculator.py
cat notes.txt | python ai_tool.py summarize -

Właśnie zbudowałeś działające narzędzie AI. Dziesięć lekcji temu zaskoczeniem było to, że input() zwraca string.

Rozszerz je

Architektura jest celowo łatwa do rozbudowy, w przybliżonej kolejności nakładu pracy:

  • Dodaj tryb. Chcesz "translate" albo "wytłumacz-jak-pięciolatkowi"? Dodaj jeden wpis do MODES z własnym promptem system. Żadnych innych zmian — to korzyść ze sterowania zachowaniem danymi (Rozdz. 3) zamiast logiką z rozgałęzieniami.
  • Dodaj flagę. Użyj argparse, by dodać --model (wybierz szybszy, tańszy model) albo --max-tokens.
  • Zapisz wynik. Zapisz rezultat do pliku przez pathlib (Rozdz. 5), gdy podano --out report.md.
  • Przetestuj czyste części. read_input i odczyt z MODES są testowalne bez żadnego wywołania API — napisz przypadki pytest (Rozdz. 7), trzymając wywołanie sieciowe odizolowane w run().

Zacząłeś od instalacji interpretera i wypisania paragonu; kończysz otypowanym, odpornym na błędy narzędziem CLI, które strumieniuje odpowiedzi AI na żywo z prawdziwego API. Bardziej niż jakąkolwiek pojedynczą bibliotekę zbudowałeś instynkty. A teraz idź i stwórz coś. 🐍

Część Praktycznego Bootcampu Pythona — buduj prawdziwe narzędzia, we właściwy sposób.

🎉

Koniec początku

Udało się

Zacząłeś od instalacji interpretera i wypisania paragonu; kończysz otypowanym, przetestowanym, odpornym na błędy narzędziem CLI, które strumieniuje odpowiedzi AI na żywo z prawdziwego API.

Co ważniejsze, wyrobiłeś sobie instynkty:

  • Konwertuj dane wejściowe na granicy programu.
  • Pozwól, by pracę wykonała struktura danych — właściwa zamienia mozół w coś natychmiastowego.
  • return-uj wartości zamiast je print-ować, żeby inny kod (i twoje testy) mogły z nich skorzystać.
  • Łap tylko te wyjątki, których się spodziewasz; pozwól prawdziwym błędom głośno paść.
  • Oddzielaj brudne operacje I/O od czystej logiki, którą da się przetestować.
  • Trzymaj sekrety z dala od kodu źródłowego.

Te nawyki przetrwają dłużej niż jakakolwiek pojedyncza biblioteka.

Dokąd dalej

Pod każdą dziedziną Pythona leży ten sam język, który teraz znasz:

  • Backendy webowe — FastAPI, Django
  • Dane i ML — pandas, polars, scikit-learn, PyTorch
  • Automatyzacja i skrypty — codzienna supermoc
  • Narzędzia AI i agenci — zacznij tam, gdzie skończył projekt finałowy

Wybierz projekt, na którym naprawdę ci zależy, i zbuduj go. Tak właśnie reszta wiedzy zostaje na stałe.

Dzięki za wspólne kodowanie. A teraz idź i stwórz coś swojego. 🐍

Przeglądaj więcej kursów

Frequently Asked Questions

Who is the Pragmatic Python Bootcamp for?

It's built for two audiences at once: absolute beginners with zero coding background, and developers moving over from C#, Java, or another statically-typed language. Beginners work through all 22 lessons top to bottom, while experienced developers can skim the fundamentals and lean on the Dev Callout boxes that map new Python behavior onto what they already know.

Do I need any prior programming experience to start?

No — the course assumes zero background and opens with installing Python and printing your first line of output. Its 22 lessons build directly on one another, and each ends with a quiz you must score at least 67% on before the next lesson unlocks, so gaps in understanding get caught early.

What will I actually build during the course?

Nine hands-on projects across 22 lessons: a tip calculator, a number-guessing game, an inventory system, a security analyzer, a log analyzer, a log cleanser, a server-fleet monitor, a live GitHub inspector, and a capstone: a typed, tested, AI-powered command-line tool that streams real API responses.

How long does the course take to complete?

The full curriculum runs about 9.5 hours of lesson content across 22 lessons in 7 chapters, plus 88 quiz questions to check understanding along the way. Individual lessons run 20 to 40 minutes each, so a full chapter comfortably fits into a single sitting.

How is the course structured, and how do the quizzes work?

Content is organized into 7 chapters and 22 lessons, each ending in a short multiple-choice quiz. The course settings require a score of at least 67% to unlock the next lesson — a fixed threshold, not just a suggestion — which keeps you from advancing past material you haven't actually understood.

What is a 'Dev Callout,' and do I have to be a total beginner to benefit from this course?

No — a Dev Callout is a highlighted box placed throughout the lessons specifically for developers coming from C#, Java, or similar typed languages. It reframes unfamiliar Python behavior, like dynamic typing, indentation-based blocks, and scope, as a direct comparison to what those developers already know, so they don't have to sit through beginner explanations.

What does the final capstone project involve?

The capstone is a typed, tested, AI-powered command-line tool that streams live responses from a real AI API. It draws on the whole course: type hints checked with mypy, automated tests written in pytest, proper error handling, and a clean separation between I/O and the logic that tests can actually exercise.

Does the course cover professional practices like testing and type checking, or just the basics?

Both. The first six chapters build core fundamentals — syntax, data structures, functions, error handling, files, and object-oriented programming — while the seventh chapter, 'Professional Python,' is dedicated to type hints with mypy and automated testing with pytest, before all of it comes together in the capstone AI CLI project.