Python & AI Tutorials Logo
Pemrograman Python

41. Debugging dan Pengujian Kode Anda

Menulis kode baru setengah dari perjuangan. Setengah lainnya adalah memastikan kode Anda bekerja dengan benar dan menemukan masalah saat tidak bekerja. Setiap programmer, dari pemula hingga ahli, menulis kode yang punya bug. Bedanya adalah programmer berpengalaman sudah mengembangkan pendekatan yang sistematis untuk menemukan dan memperbaiki bug tersebut.

Di bab ini, Anda akan mempelajari teknik debugging praktis yang membantu Anda memahami apa yang sebenarnya dilakukan kode Anda, menemukan masalah dengan cepat, dan memverifikasi bahwa kode Anda bekerja sesuai yang diinginkan. Keterampilan ini akan membuat Anda jadi programmer yang lebih percaya diri dan efektif.

41.1) Membaca Tracebacks untuk Menemukan Letak Error (Ulasan Singkat)

Seperti yang kita pelajari di Bab 24, Python menyediakan pesan error yang detail yang disebut tracebacks ketika sesuatu berjalan salah. Mari kita ulas cara membacanya dengan efektif, karena ini adalah lini pertahanan pertama Anda saat debugging.

41.1.1) Anatomi Sebuah Traceback

Ketika Python menemui error, Python menunjukkan tepat di mana masalah terjadi dan jenis error-nya. Berikut contoh traceback yang umum:

python
def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    return total / count
 
def process_student_grades(grades):
    average = calculate_average(grades)
    return f"Average: {average:.1f}"
 
# Ini akan menyebabkan error
student_grades = []
result = process_student_grades(student_grades)
print(result)

Output:

Traceback (most recent call last):
  File "grades.py", line 12, in <module>
    result = process_student_grades(student_grades)
  File "grades.py", line 7, in process_student_grades
    average = calculate_average(grades)
  File "grades.py", line 4, in calculate_average
    return total / count
           ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Mari kita uraikan apa yang diberitahu traceback ini:

Baris 12: process_student_grades dipanggil

Baris 7: calculate_average dipanggil

Baris 4: Operasi pembagian

ZeroDivisionError: division by zero

Membaca dari bawah ke atas:

  1. Jenis error dan pesannya (paling bawah): ZeroDivisionError: division by zero memberi tahu kita dengan tepat apa yang salah
  2. Baris yang tepat tempat error terjadi: return total / count pada baris 4
  3. Rantai pemanggilan (call chain) yang menunjukkan bagaimana kita sampai ke sana: mulai di baris 12, lewat baris 7, berakhir di baris 4

41.1.2) Menggunakan Tracebacks untuk Menemukan Akar Masalah

Traceback menunjukkan gejala (di mana error terjadi), tetapi Anda perlu menemukan penyebabnya (kenapa itu terjadi). Mari telusuri masalahnya:

python
# Error terjadi di sini
return total / count  # count bernilai 0
 
# Tapi masalah sebenarnya ada di sini
student_grades = []  # List kosong diteruskan ke fungsi

Pembagian dengan nol terjadi karena kita meneruskan list kosong. Traceback menunjuk ke baris 4, tetapi perbaikannya perlu dilakukan lebih awal—entah dengan memvalidasi input atau menangani kasus list kosong:

python
def calculate_average(numbers):
    """Mengembalikan rata-rata dari numbers, atau None jika list kosong."""
    if not numbers:
        return None
    return sum(numbers) / len(numbers)
 
def process_student_grades(grades):
    """Memproses nilai siswa dan mengembalikan string yang sudah diformat."""
    average = calculate_average(grades)
    if average is None:
        return "No grades to process"
    return f"Average: {average:.1f}"
 
# Sekarang ini bekerja dengan aman
student_grades = []
result = process_student_grades(student_grades)
print(result)  # Output: No grades to process
 
# Dan ini juga bekerja
student_grades = [85, 92, 78, 90]
result = process_student_grades(student_grades)
print(result)  # Output: Average: 86.2

Poin Penting:

  • Baca tracebacks dari bawah ke atas
  • Lokasi error (gejala) tidak selalu merupakan akar penyebabnya
  • Validasi input sejak awal untuk mencegah error di kemudian hari
  • Gunakan defensive programming (.get(), pemeriksaan panjang) untuk kode yang lebih aman

Berbagai jenis error menghasilkan traceback yang berbeda, tetapi proses membacanya selalu sama: mulai dari bawah untuk melihat apa yang salah, lalu telusuri ke atas untuk memahami bagaimana Anda sampai ke sana. Jika Anda perlu mengingat kembali tipe exception tertentu, lihat lagi Bab 24.

Sekarang Anda bisa membaca tracebacks dengan efektif, mari pelajari cara menelusuri kode secara mental untuk memahami apa yang dilakukannya langkah demi langkah.

41.2) Menelusuri Eksekusi Kode Secara Mental

Kadang Anda menemukan bug tetapi tidak bisa langsung menjalankan kodenya—mungkin Anda sedang meninjau kode di kertas, membaca pull request orang lain, atau mencoba memahami kenapa sebuah fungsi(function) berperilaku tidak seperti yang diharapkan. Dalam situasi ini, eksekusi mental (mental execution)—menelusuri kode baris demi baris di kepala Anda, melacak apa yang terjadi pada setiap variabel—menjadi sangat berharga.

Bahkan programmer berpengalaman memakai teknik ini secara rutin. Sebelum menambahkan print statement atau menjalankan debugger, mereka sering menelusuri beberapa iterasi secara mental untuk membentuk hipotesis tentang di mana kira-kira masalahnya. Ini lebih cepat daripada trial-and-error dan membantu Anda memahami kode lebih dalam.

Eksekusi mental sangat berguna ketika:

  • Membaca kode yang tidak familiar untuk memahami apa yang dilakukannya
  • Meninjau fungsi(function) kecil (5-15 baris) sebelum menjalankannya
  • Debugging error logika ketika kode berjalan tetapi menghasilkan hasil yang salah
  • Memahami perilaku loop (loop) ketika polanya tidak langsung jelas
  • Code review ketika Anda tidak mudah menjalankan kodenya sendiri

Untuk kode yang lebih besar atau lebih kompleks, Anda akan menggabungkan penelusuran mental dengan teknik lain yang akan kita bahas nanti di bab ini. Namun menguasai keterampilan ini akan membuat Anda jadi debugger yang jauh lebih efektif.

41.2.1) Proses Eksekusi Mental

Ketika Anda mengeksekusi kode secara mental, Anda berperan sebagai interpreter Python, mengikuti aturan yang sama seperti Python. Mari berlatih dengan contoh sederhana:

python
def find_maximum(numbers):
    max_value = numbers[0]
    for num in numbers:
        if num > max_value:
            max_value = num
    return max_value
 
result = find_maximum([3, 7, 2, 9, 5])
print(result)  # Output: 9

Berikut cara menelusuri kode ini:

Penelusuran langkah demi langkah:

Initial state:
  numbers = [3, 7, 2, 9, 5]
  max_value = 3  (numbers[0])
 
Iteration 1: num = 3
  Check: 3 > 3? → False
  max_value remains 3
 
Iteration 2: num = 7
  Check: 7 > 3? → True
  max_value = 7 ✓
 
Iteration 3: num = 2
  Check: 2 > 7? → False
  max_value remains 7
 
Iteration 4: num = 9
  Check: 9 > 7? → True
  max_value = 9 ✓
 
Iteration 5: num = 5
  Check: 5 > 9? → False
  max_value remains 9
 
Return: 9

41.2.2) Membuat Tabel Jejak (Trace Table)

Untuk kode yang lebih kompleks, buat tabel jejak (trace table) yang menunjukkan bagaimana variabel berubah seiring waktu. Ini sangat membantu untuk loop dan struktur bertingkat:

python
def calculate_running_totals(numbers):
    totals = []
    running_sum = 0
    for num in numbers:
        running_sum += num
        totals.append(running_sum)
    return totals
 
result = calculate_running_totals([10, 20, 30, 40])
print(result)  # Output: [10, 30, 60, 100]

Tabel jejak:

Tabel ini menunjukkan keadaan variabel di setiap langkah. Perhatikan bagaimana running_sum berubah dari "sebelum" ke "sesudah" setiap penjumlahan:

Iterationnumrunning_sum (before)running_sum (after)totals
Start-00[]
110010[10]
2201030[10, 30]
3303060[10, 30, 60]
44060100[10, 30, 60, 100]

Membuat tabel ini membantu Anda melihat secara persis bagaimana data mengalir melalui kode Anda. Jika output tidak sesuai yang Anda harapkan, Anda bisa menunjukkan dengan tepat di mana semuanya mulai salah.

41.2.3) Menelusuri Logika Kondisional

Pernyataan kondisional membutuhkan perhatian yang cermat terhadap cabang mana yang dieksekusi. Mari telusuri contoh yang lebih kompleks:

python
def categorize_grade(score):
    if score >= 90:
        category = "Excellent"
        bonus = 10
    elif score >= 80:
        category = "Good"
        bonus = 5
    elif score >= 70:
        category = "Satisfactory"
        bonus = 0
    else:
        category = "Needs Improvement"
        bonus = 0
    
    final_score = score + bonus
    return category, final_score
 
result = categorize_grade(85)
print(result)  # Output: ('Good', 90)

Jejak mental untuk score = 85:

  1. Cek 85 >= 90 → False, lewati blok pertama
  2. Cek 85 >= 80 → True, masuk blok kedua
  3. Set category = "Good" dan bonus = 5
  4. Lewati blok elif dan else sisanya (sudah menemukan yang cocok)
  5. Hitung final_score = 85 + 5 = 90
  6. Return ("Good", 90)

41.2.4) Menelusuri Pemanggilan Fungsi dan Return

Ketika fungsi(function) memanggil fungsi lain, Anda perlu melacak call stack—urutan pemanggilan fungsi dan variabel lokalnya:

python
def calculate_tax(amount, rate):
    tax = amount * rate
    return tax
 
def calculate_total(price, quantity, tax_rate):
    subtotal = price * quantity
    tax = calculate_tax(subtotal, tax_rate)
    total = subtotal + tax
    return total
 
result = calculate_total(50, 3, 0.08)
print(f"Total: ${result:.2f}")  # Output: Total: $162.00

Jejak dengan call stack:

┌─ calculate_total(50, 3, 0.08)
│  price = 50, quantity = 3, tax_rate = 0.08
│  subtotal = 150

│  ┌─ calculate_tax(150, 0.08)
│  │  amount = 150, rate = 0.08
│  │  tax = 12.0
│  │  return 12.0
│  └─

│  tax = 12.0 (from calculate_tax)
│  total = 162.0
│  return 162.0
└─
 
result = 162.0

Jejak langkah demi langkah ini menunjukkan secara persis bagaimana data mengalir antar fungsi. Saat debugging, jika hasil akhirnya salah, Anda bisa menelusuri balik untuk melihat fungsi mana yang menghasilkan nilai perantara yang keliru.

Penelusuran mental itu kuat, tetapi untuk kode kompleks bisa terasa melelahkan. Di bagian berikutnya, kita akan belajar cara menggunakan print statement secara strategis untuk melihat apa yang benar-benar terjadi saat kode berjalan, yang sering kali lebih cepat dan lebih andal daripada eksekusi mental saja.

41.3) Debugging dengan Print: f"{var=}" dan repr()

Walaupun eksekusi mental bekerja dengan baik untuk fungsi kecil, itu jadi tidak praktis untuk kode yang lebih besar atau lebih kompleks. Saat Anda tidak yakin apa yang terjadi di dalam sebuah loop, atau ketika sebuah perhitungan menghasilkan hasil yang tidak terduga, cara tercepat untuk menyelidiki sering kali adalah menambahkan print() secara strategis.

Print debugging punya beberapa keunggulan dibanding teknik lain:

  • Tidak perlu tools khusus: Bekerja di lingkungan Python mana pun
  • Cepat diterapkan: Tambahkan print statement dalam hitungan detik
  • Output jelas: Anda melihat persis apa yang Anda minta
  • Mudah dihapus: Hapus print setelah selesai

Developer profesional menggunakan print debugging setiap saat—ini bukan teknik "pemula". Mari pelajari cara memakainya dengan efektif.

41.3.1) Print Debugging Dasar

Pendekatan debugging paling sederhana adalah mencetak nilai variabel di titik-titik penting dalam kode Anda:

python
def process_order(items, discount_rate):
    print(f"Starting process_order")
    print(f"Items: {items}")
    print(f"Discount rate: {discount_rate}")
    
    subtotal = sum(item['price'] * item['quantity'] for item in items)
    print(f"Subtotal: {subtotal}")
    
    discount = subtotal * discount_rate
    print(f"Discount amount: {discount}")
    
    total = subtotal - discount
    print(f"Final total: {total}")
    
    return total
 
order_items = [
    {'name': 'Book', 'price': 25.99, 'quantity': 2},
    {'name': 'Pen', 'price': 3.50, 'quantity': 5}
]
 
result = process_order(order_items, 0.10)

Output:

Starting process_order
Items: [{'name': 'Book', 'price': 25.99, 'quantity': 2}, {'name': 'Pen', 'price': 3.5, 'quantity': 5}]
Discount rate: 0.1
Subtotal: 69.47999999999999
Discount amount: 6.9479999999999995
Final total: 62.53199999999999

Print statement ini menunjukkan alur eksekusi dan nilainya di setiap langkah. Jika hasil akhirnya salah, Anda bisa melihat dengan tepat di mana perhitungannya mulai melenceng.

41.3.2) Menggunakan f"{var=}" untuk Inspeksi Cepat

Python 3.8 memperkenalkan sintaks debugging yang praktis: f"{var=}". Ini mencetak nama variabel sekaligus nilainya:

python
def calculate_compound_interest(principal, rate, years):
    # Pendekatan tradisional
    print(f"principal: {principal}")
    print(f"rate: {rate}")
    print(f"years: {years}")
    
    # Pendekatan yang lebih rapi dengan f"{var=}"
    print(f"{principal=}")
    print(f"{rate=}")
    print(f"{years=}")
    
    # Anda bisa memakai ekspresi, bukan hanya variabel
    print(f"{principal * rate=}")
    print(f"{(1 + rate) ** years=}")
    
    amount = principal * (1 + rate) ** years
    print(f"{amount=}")
    
    return amount
 
result = calculate_compound_interest(1000, 0.05, 10)

Output:

principal: 1000
rate: 0.05
years: 10
principal=1000
rate=0.05
years=10
principal * rate=50.0
(1 + rate) ** years=1.628894626777442
amount=1628.894626777442

41.3.3) Menggunakan repr() untuk Melihat Bentuk Data yang Sebenarnya

Kadang apa yang Anda lihat saat dicetak bukanlah yang Anda kira. Fungsi repr() menunjukkan representasi persis dari sebuah objek, termasuk karakter tersembunyi:

python
# String ini terlihat sama ketika dicetak
text1 = "Hello"
text2 = "Hello\n"  # Punya newline di akhir
 
print("Using print():")
print(f"text1: {text1}")
print(f"text2: {text2}")
 
print("\nUsing repr():")
print(f"text1: {repr(text1)}")
print(f"text2: {repr(text2)}")

Output:

Using print():
text1: Hello
text2: Hello
 
Using repr():
text1: 'Hello'
text2: 'Hello\n'

Output repr() menunjukkan bahwa text2 punya karakter newline tersembunyi. Ini sangat krusial saat debugging pemrosesan string:

python
def clean_user_input():
    # Input pengguna sering punya whitespace tersembunyi
    username = input("Enter username: ")  # Pengguna mengetik "Alice  "
    
    print(f"Username with print(): {username}")
    print(f"Username with repr(): {repr(username)}")
    
    # Bersihkan input
    cleaned = username.strip()
    print(f"Cleaned with repr(): {repr(cleaned)}")
    
    return cleaned

Jika pengguna mengetik "Alice" lalu spasi dan menekan Enter, Anda mungkin melihat:

Output:

Enter username: Alice  
Username with print(): Alice  
Username with repr(): 'Alice  '
Cleaned with repr(): 'Alice'

Output repr() mengungkap spasi di akhir yang tidak ditampilkan dengan jelas oleh print().

Kapan memakai repr() vs str():

repr() dirancang untuk developer—ia menunjukkan representasi string "resmi" yang bisa merekonstruksi objek. str() (yang dipakai print() secara default) dirancang untuk end user—ia menunjukkan versi yang mudah dibaca dan ramah.

Untuk debugging, repr() biasanya lebih membantu karena ia mengungkap struktur data yang sebenarnya.

41.3.4) Penempatan Print yang Strategis

Jangan asal menyebar print statement di mana-mana. Letakkan secara strategis:

python
def calculate_shipping_cost(weight, distance, express=False):
    print(f"=== calculate_shipping_cost called ===")
    print(f"Input: {weight=}, {distance=}, {express=}")
    
    # Hitung biaya dasar
    base_rate = 0.50
    base_cost = weight * distance * base_rate
    print(f"Calculated: {base_cost=}")
    
    # Terapkan biaya tambahan express
    if express:
        surcharge = base_cost * 0.50
        print(f"Express surcharge: {surcharge=}")
        total = base_cost + surcharge
    else:
        print("No express surcharge")
        total = base_cost
    
    print(f"Final: {total=}")
    print(f"=== calculate_shipping_cost returning ===\n")
    return total
 
# Uji berbagai skenario
cost1 = calculate_shipping_cost(10, 500, express=True)
cost2 = calculate_shipping_cost(5, 200, express=False)

Output:

=== calculate_shipping_cost called ===
Input: weight=10, distance=500, express=True
Calculated: base_cost=2500.0
Express surcharge: surcharge=1250.0
Final: total=3750.0
=== calculate_shipping_cost returning ===
 
=== calculate_shipping_cost called ===
Input: weight=5, distance=200, express=False
Calculated: base_cost=500.0
No express surcharge
Final: total=500.0
=== calculate_shipping_cost returning ===

Penanda yang jelas (===) dan output yang terorganisir membuat alur eksekusi mudah diikuti.

41.3.5) Menghapus Print Debug

Setelah Anda menemukan dan memperbaiki bug, ingat untuk menghapus print debug Anda. Berikut beberapa strateginya:

Strategi 1: Gunakan prefix yang khas

python
# Mudah ditemukan dan dihapus dengan search/replace
print(f"DEBUG: {total=}")
print(f"DEBUG: {items=}")

Strategi 2: Gunakan debug flag

python
DEBUG = True
 
def calculate_total(items):
    if DEBUG:
        print(f"Processing {len(items)} items")
    
    total = sum(item['price'] for item in items)
    
    if DEBUG:
        print(f"{total=}")
    
    return total
 
# Matikan semua output debug sekaligus
DEBUG = False

Strategi 3: Comment out tapi simpan

python
def process_data(data):
    # print(f"DEBUG: {data=}")  # Berguna untuk debugging di masa depan
    result = transform(data)
    # print(f"DEBUG: {result=}")
    return result

Untuk logging yang lebih canggih dan bisa Anda biarkan di kode produksi, Python punya modul logging, tetapi print statement sederhana sangat cocok untuk debugging cepat saat development.

Print debugging menunjukkan nilai variabel, tetapi kadang Anda perlu memahami struktur sebuah objek—method apa yang dimilikinya, tipe apa itu, dan apa yang bisa dilakukannya. Di bagian berikutnya, kita akan belajar cara menginspeksi objek dengan type() dan dir().

41.4) Menginspeksi Objek: type() dan dir()

Print debugging menunjukkan nilai variabel Anda, tetapi kadang masalahnya bukan nilainya—melainkan tipe objek yang Anda gunakan. Anda mungkin mengira itu list tetapi ternyata string, atau Anda bekerja dengan objek yang tidak familiar dan tidak tahu method apa saja yang didukung.

Python menyediakan tool bawaan untuk menginspeksi objek: type() memberi tahu Anda objek itu jenis apa, dan dir() menunjukkan operasi apa saja yang didukung. Fungsi ini penting ketika:

  • Debugging error terkait tipe (TypeError, AttributeError)
  • Bekerja dengan library atau API yang tidak familiar
  • Memahami objek yang dikembalikan oleh kode pihak ketiga
  • Memverifikasi bahwa kode Anda menerima tipe yang diharapkan

Mari pelajari cara menggunakan tool inspeksi ini dengan efektif.

41.4.1) Menggunakan type() untuk Mengidentifikasi Tipe Objek

Fungsi type() memberi tahu Anda dengan tepat objek itu jenis apa. Ini krusial saat debugging error terkait tipe:

python
def process_data(data):
    print(f"Received data: {data}")
    print(f"Data type: {type(data)}")
    
    if isinstance(data, list):
        print("Processing as list")
        return sum(data)
    elif isinstance(data, dict):
        print("Processing as dictionary")
        return sum(data.values())
    else:
        print("Unexpected type!")
        return None
 
# Uji dengan tipe yang berbeda
result1 = process_data([10, 20, 30])
print(f"Result: {result1}\n")
 
result2 = process_data({'a': 10, 'b': 20, 'c': 30})
print(f"Result: {result2}\n")
 
result3 = process_data("123")
print(f"Result: {result3}")

Output:

Received data: [10, 20, 30]
Data type: <class 'list'>
Processing as list
Result: 60
 
Received data: {'a': 10, 'b': 20, 'c': 30}
Data type: <class 'dict'>
Processing as dictionary
Result: 60
 
Received data: 123
Data type: <class 'str'>
Unexpected type!
Result: None

41.4.2) Debugging Kebingungan Tipe

Kebingungan tipe adalah sumber bug yang umum, terutama ketika bekerja dengan fungsi(function) yang bisa menerima data dari berbagai sumber—input pengguna, pembacaan file, respons API, atau fungsi lain. Anda mungkin mengira menerima list angka tetapi ternyata string, atau mengira dictionary tetapi yang diterima list.

Menggunakan type() membantu mengidentifikasi kapan Anda punya tipe yang salah. Dengan mencetak tipe sejak awal fungsi, Anda bisa langsung melihat ketidaksesuaian tipe sebelum itu memicu pesan error yang membingungkan lebih dalam di kode Anda:

python
def calculate_average(numbers):
    print(f"{type(numbers)=}")
    print(f"{numbers=}")  # Tunjukkan apa yang sebenarnya kita terima
    
    # Ini akan gagal jika numbers bukan list berisi angka
    total = sum(numbers)
    count = len(numbers)
    return total / count
 
# Kesalahan umum: lupa mengonversi string ke list
scores = "85"  # Seharusnya [85] atau cukup 85
try:
    avg = calculate_average(scores)
    print(f"Average: {avg}")
except TypeError as e:
    print(f"TypeError: {e}")
    print(f"Expected list of numbers, got {type(scores)}")
    print(f"The string contains: {repr(scores)}")

Output:

type(numbers)=<class 'str'>
numbers='85'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Expected list of numbers, got <class 'str'>
The string contains: '85'

Pengecekan type() langsung mengungkap masalahnya: kita mengirim string padahal yang dibutuhkan list. Tanpa output debug ini, Anda mungkin menghabiskan waktu untuk mencoba memahami kenapa sum() gagal, padahal masalah sebenarnya adalah tipe data yang salah masuk ke fungsi sejak awal.

41.4.3) Menggunakan dir() untuk Menemukan Method yang Tersedia

Saat bekerja dengan objek yang tidak familiar—entah dari library yang sedang Anda pelajari, respons API, atau bahkan tipe bawaan Python—Anda sering perlu tahu: "Apa yang bisa saya lakukan dengan objek ini?" Fungsi dir() menjawab pertanyaan ini dengan menampilkan semua atribut dan method yang tersedia pada objek.

Ini sangat berharga ketika:

  • Anda sedang mengeksplorasi library baru dan ingin melihat method apa yang disediakan oleh sebuah objek
  • Anda menerima objek dari kode pihak ketiga dan perlu memahami kemampuannya
  • Anda lupa nama tepat sebuah method yang ingin digunakan
  • Anda sedang debugging dan ingin memverifikasi bahwa objek memiliki method yang Anda harapkan

Mari eksplor method apa saja yang dimiliki string:

python
# Mengeksplor method apa saja yang dimiliki string
text = "Python Programming"
 
print(f"Type: {type(text)}")
print(f"\nAvailable string methods (showing first 10):")
methods = [m for m in dir(text) if not m.startswith('_')]
for method in methods[:10]:  # Tampilkan 10 pertama
    print(f"  {method}")
print(f"  ... and {len(methods) - 10} more")

Output:

Type: <class 'str'>
 
Available string methods (showing first 10):
  capitalize
  casefold
  center
  count
  encode
  endswith
  expandtabs
  find
  format
  format_map
  ... and 37 more

Sekarang Anda bisa melihat semua operasi yang tersedia pada string. Jika Anda tidak yakin apakah string punya method count atau endswith, dir() menunjukkan bahwa keduanya ada. Anda lalu bisa menggunakan fungsi help() Python untuk mempelajari lebih jauh method tertentu:

python
# Pelajari lebih lanjut tentang method tertentu
help(text.count)

Ini akan menampilkan dokumentasi untuk method count:

Help on built-in function count:
 
count(sub[, start[, end]], /) method of builtins.str instance
    Return the number of non-overlapping occurrences of substring sub in string S[start:end].
 
    Optional arguments start and end are interpreted as in slice notation.

Fungsi dir() seperti punya dokumentasi yang tertanam langsung di Python—ia menunjukkan apa saja yang mungkin dilakukan dengan objek apa pun yang sedang Anda pakai.

41.4.4) Menginspeksi Objek Kustom

Saat bekerja dengan class kustom, type() dan dir() membantu Anda memahami apa yang sedang Anda hadapi. Selain itu, Python menyediakan hasattr() untuk mengecek apakah sebuah objek punya atribut tertentu sebelum mencoba mengaksesnya—ini mencegah exception AttributeError.

python
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    def get_status(self):
        return "Passing" if self.grade >= 60 else "Failing"
 
student = Student("Alice", 85)
 
print(f"Object type: {type(student)}")
print(f"\nAvailable attributes and methods:")
for attr in dir(student):
    if not attr.startswith('_'):
        print(f"  {attr}")
 
# Cek apakah atribut tertentu ada
print(f"\nHas 'name' attribute: {hasattr(student, 'name')}")
print(f"Has 'age' attribute: {hasattr(student, 'age')}")
print(f"Has 'get_status' method: {hasattr(student, 'get_status')}")
 
# Sekarang kita bisa mengakses atribut yang kita tahu ada dengan aman
if hasattr(student, 'name'):
    print(f"\nStudent name: {student.name}")
else:
    print("\nNo name attribute found")
 
if hasattr(student, 'get_status'):
    print(f"Status: {student.get_status()}")
else:
    print("No get_status method found")
 
# Ini mencegah error seperti ini:
# print(student.age)  # Would raise AttributeError!

Output:

Object type: <class '__main__.Student'>
 
Available attributes and methods:
  get_status
  grade
  name
 
Has 'name' attribute: True
Has 'age' attribute: False
Has 'get_status' method: True
 
Student name: Alice
Status: Passing

Fungsi hasattr() penting untuk menulis kode defensif—kode yang mengecek apakah operasi aman sebelum menjalankannya. Fungsi ini mengembalikan True jika atribut ada, False jika tidak—memungkinkan Anda mengambil keputusan sebelum mencoba mengakses atribut. Ini sangat penting saat bekerja dengan objek dari library eksternal atau input pengguna ketika Anda tidak bisa menjamin atribut apa saja yang akan ada.

41.4.5) Menggunakan getattr() untuk Akses Atribut yang Aman

Saat Anda tidak yakin apakah suatu atribut ada, gunakan getattr() dengan nilai default:

python
def display_student_info(student):
    """Menampilkan info siswa dengan aman walaupun beberapa atribut tidak ada."""
    print(f"Type: {type(student)}")
    
    # Akses atribut aman dengan default
    name = getattr(student, 'name', 'Unknown')
    grade = getattr(student, 'grade', 0)
    age = getattr(student, 'age', 'Not specified')
    
    print(f"Name: {name}")
    print(f"Grade: {grade}")
    print(f"Age: {age}")
    
    # Cek apakah method ada sebelum memanggil
    if hasattr(student, 'get_status'):
        status = student.get_status()
        print(f"Status: {status}")
 
# Menggunakan class Student yang sama seperti di atas
student = Student("Bob", 72)
display_student_info(student)

Output:

Type: <class '__main__.Student'>
Name: Bob
Grade: 72
Age: Not specified
Status: Passing

Pendekatan ini mencegah exception AttributeError saat bekerja dengan objek yang mungkin tidak punya semua atribut yang diharapkan. Fungsi getattr() sangat berguna ketika:

  • Bekerja dengan objek dari API eksternal yang mungkin memiliki versi berbeda
  • Menangani atribut opsional dalam class Anda sendiri
  • Membangun kode defensif yang menangani data yang hilang dengan elegan

Memahami tipe objek yang Anda miliki dan method apa yang didukung sangat krusial untuk debugging. Namun terkadang Anda perlu memverifikasi bukan hanya bahwa kode berjalan, tetapi juga bahwa ia menghasilkan hasil yang benar. Di bagian berikutnya, kita akan belajar cara menggunakan pernyataan assert untuk menguji asumsi Anda dan menangkap bug lebih awal.

41.5) Menguji dengan Pernyataan assert

Kita sudah belajar cara debugging kode ketika sesuatu berjalan salah—membaca tracebacks, menelusuri eksekusi secara mental, menggunakan print statement, dan menginspeksi objek. Namun ada pendekatan yang lebih baik daripada memperbaiki bug setelah muncul: mencegahnya sejak awal melalui pengujian.

Pernyataan assert adalah tool pengujian paling sederhana di Python. Ia memungkinkan Anda memverifikasi bahwa kode Anda berperilaku benar dengan mengecek asumsi di titik-titik kritis. Ketika sebuah assertion gagal, Python langsung memberi tahu persis apa yang salah dan di mana, sehingga jauh lebih mudah menangkap bug lebih awal—sering kali bahkan sebelum Anda menjalankan program utama.

Assertion sangat berharga untuk:

  • Memverifikasi bahwa fungsi menghasilkan hasil yang diharapkan
  • Mengecek bahwa input memenuhi kebutuhan Anda
  • Menguji edge case yang bisa merusak kode Anda
  • Mendokumentasikan asumsi yang diandalkan kode Anda

Anggap assertion sebagai pemeriksaan otomatis yang terus-menerus memverifikasi bahwa kode Anda bekerja sesuai yang diinginkan. Mari pelajari cara menggunakannya dengan efektif.

41.5.1) Apa yang Dilakukan assert

Pernyataan assert mengecek apakah sebuah kondisi bernilai true. Jika kondisinya true, tidak terjadi apa pun—kode berjalan normal. Jika false, Python melempar AssertionError dan menghentikan eksekusi.

Sintaks:

python
assert condition, "Optional error message"
  • condition: Ekspresi apa pun yang dievaluasi menjadi True atau False
  • "Optional error message": Teks bantuan yang ditampilkan ketika assertion gagal

Berikut cara kerjanya dalam praktik:

python
# Assertion sederhana
x = 10
assert x > 0  # Lolos tanpa suara (x memang > 0)
assert x < 5  # Gagal! Memunculkan AssertionError
 
# Dengan pesan error (jauh lebih membantu!)
assert x > 0, f"x must be positive, got {x}"
assert x < 5, f"x must be less than 5, got {x}"  # Gagal dengan pesan yang jelas

Sekarang mari lihat assertion di fungsi nyata:

python
def calculate_discount(price, discount_percent):
    # Verifikasi input valid
    assert price >= 0, "Price cannot be negative"
    assert 0 <= discount_percent <= 100, "Discount must be between 0 and 100"
    
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    
    # Verifikasi output masuk akal
    assert final_price >= 0, "Final price cannot be negative"
    
    return final_price
 
# Input valid berjalan baik
result = calculate_discount(100, 20)
print(f"Price after 20% discount: ${result}")  # Output: Price after 20% discount: $80.0
 
# Input tidak valid memicu assertion
try:
    result = calculate_discount(-50, 20)
except AssertionError as e:
    print(f"Assertion failed: {e}")  # Output: Assertion failed: Price cannot be negative
 
try:
    result = calculate_discount(100, 150)
except AssertionError as e:
    print(f"Assertion failed: {e}")  # Output: Assertion failed: Discount must be between 0 and 100

41.5.2) Menggunakan Assertion untuk Memverifikasi Perilaku Fungsi

Assertion sangat bagus untuk menguji bahwa fungsi menghasilkan hasil yang diharapkan:

python
def calculate_average(numbers):
    if not numbers:
        return 0.0
    return sum(numbers) / len(numbers)
 
# Uji dengan berbagai input
result = calculate_average([10, 20, 30])
assert result == 20.0, f"Expected 20.0, got {result}"
print(f"Test 1 passed: average of [10, 20, 30] = {result}")
 
result = calculate_average([5, 5, 5, 5])
assert result == 5.0, f"Expected 5.0, got {result}"
print(f"Test 2 passed: average of [5, 5, 5, 5] = {result}")
 
result = calculate_average([])
assert result == 0.0, f"Expected 0.0 for empty list, got {result}"
print(f"Test 3 passed: average of [] = {result}")
 
result = calculate_average([100])
assert result == 100.0, f"Expected 100.0, got {result}"
print(f"Test 4 passed: average of [100] = {result}")

Output:

Test 1 passed: average of [10, 20, 30] = 20.0
Test 2 passed: average of [5, 5, 5, 5] = 5.0
Test 3 passed: average of [] = 0.0
Test 4 passed: average of [100] = 100.0

Jika ada assertion yang gagal, Anda langsung tahu test case mana yang mengungkap masalahnya.

41.5.3) Menguji Edge Case

Edge case adalah input pada batas-batas dari apa yang seharusnya bisa ditangani fungsi Anda. Menguji ini mengungkap bug yang mungkin luput dari input normal:

python
def get_first_and_last(items):
    """Mengembalikan item pertama dan terakhir dari sebuah sequence."""
    assert len(items) > 0, "Cannot get first and last from empty sequence"
    return items[0], items[-1]
 
# Uji kasus normal
result = get_first_and_last([1, 2, 3, 4, 5])
assert result == (1, 5), f"Expected (1, 5), got {result}"
print(f"Normal case: {result}")
 
# Uji edge case: satu item
result = get_first_and_last([42])
assert result == (42, 42), f"Expected (42, 42), got {result}"
print(f"Single item: {result}")
 
# Uji edge case: dua item
result = get_first_and_last([10, 20])
assert result == (10, 20), f"Expected (10, 20), got {result}"
print(f"Two items: {result}")
 
# Uji edge case: sequence kosong (seharusnya gagal)
try:
    result = get_first_and_last([])
    print("ERROR: Should have raised AssertionError for empty list")
except AssertionError as e:
    print(f"Empty list correctly rejected: {e}")

Output:

Normal case: (1, 5)
Single item: (42, 42)
Two items: (10, 20)
Empty list correctly rejected: Cannot get first and last from empty sequence

41.5.4) Menguji Transformasi Data

Ketika fungsi Anda mentransformasikan data, pastikan transformasinya benar:

python
def remove_duplicates(items):
    """Menghapus duplikat sambil mempertahankan urutan."""
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result
 
# Uji penghapusan duplikat dasar
input_data = [1, 2, 2, 3, 1, 4, 3, 5]
result = remove_duplicates(input_data)
expected = [1, 2, 3, 4, 5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 1 passed: {input_data} -> {result}")
 
# Uji bahwa urutan dipertahankan
input_data = [3, 1, 2, 1, 3, 2]
result = remove_duplicates(input_data)
expected = [3, 1, 2]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 2 passed: {input_data} -> {result}")
 
# Uji tanpa duplikat
input_data = [1, 2, 3, 4, 5]
result = remove_duplicates(input_data)
assert result == input_data, f"Expected {input_data}, got {result}"
print(f"Test 3 passed: {input_data} -> {result}")
 
# Uji semua duplikat
input_data = [5, 5, 5, 5]
result = remove_duplicates(input_data)
expected = [5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 4 passed: {input_data} -> {result}")

Output:

Test 1 passed: [1, 2, 2, 3, 1, 4, 3, 5] -> [1, 2, 3, 4, 5]
Test 2 passed: [3, 1, 2, 1, 3, 2] -> [3, 1, 2]
Test 3 passed: [1, 2, 3, 4, 5] -> [1, 2, 3, 4, 5]
Test 4 passed: [5, 5, 5, 5] -> [5]

41.5.5) Membuat Fungsi Pengujian Sederhana

Seiring kode Anda bertambah besar, menyebarkan pernyataan assert di seluruh kode utama menjadi berantakan dan sulit dikelola. Pendekatan yang lebih baik adalah mengorganisasi pengujian Anda ke dalam fungsi pengujian khusus. Ini memisahkan kode pengujian dari kode produksi dan membuat Anda mudah menjalankan semua pengujian sekaligus.

Kenapa memakai fungsi pengujian khusus?

  • Organisasi: Semua test untuk sebuah fungsi ada di satu tempat
  • Dapat digunakan ulang: Jalankan test kapan pun Anda mengubah kode
  • Dokumentasi: Test menunjukkan bagaimana fungsi seharusnya berperilaku
  • Debugging: Saat test gagal, Anda langsung tahu skenario mana yang rusak
  • Alur kerja development: Test dulu, lalu implementasikan atau perbaiki kodenya

Mari lihat praktiknya:

python
def calculate_grade(score):
    """Mengonversi nilai numerik menjadi nilai huruf."""
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    elif score >= 60:
        return 'D'
    else:
        return 'F'
 
def test_calculate_grade():
    """Menguji fungsi calculate_grade.
    
    Fungsi ini menguji semua perilaku yang diharapkan:
    - Setiap rentang nilai (A, B, C, D, F)
    - Nilai batas (90, 80, 70, 60)
    - Edge case (sedikit di bawah setiap batas)
    """
    print("Testing calculate_grade...")
    
    # Uji nilai A
    assert calculate_grade(95) == 'A', "95 should be A"
    assert calculate_grade(90) == 'A', "90 should be A (boundary)"
    print("  ✓ A grades: passed")
    
    # Uji nilai B
    assert calculate_grade(85) == 'B', "85 should be B"
    assert calculate_grade(80) == 'B', "80 should be B (boundary)"
    print("  ✓ B grades: passed")
    
    # Uji nilai C
    assert calculate_grade(75) == 'C', "75 should be C"
    assert calculate_grade(70) == 'C', "70 should be C (boundary)"
    print("  ✓ C grades: passed")
    
    # Uji nilai D
    assert calculate_grade(65) == 'D', "65 should be D"
    assert calculate_grade(60) == 'D', "60 should be D (boundary)"
    print("  ✓ D grades: passed")
    
    # Uji nilai F
    assert calculate_grade(55) == 'F', "55 should be F"
    assert calculate_grade(0) == 'F', "0 should be F"
    print("  ✓ F grades: passed")
    
    # Uji edge case batas (satu di bawah tiap ambang)
    assert calculate_grade(89) == 'B', "89 should be B (just below A)"
    assert calculate_grade(79) == 'C', "79 should be C (just below B)"
    assert calculate_grade(69) == 'D', "69 should be D (just below C)"
    assert calculate_grade(59) == 'F', "59 should be F (just below D)"
    print("  ✓ Boundary cases: passed")
    
    print("All tests passed! ✓\n")
 
# Run the tests
test_calculate_grade()
 
# Sekarang Anda bisa memakai fungsinya dengan percaya diri
student_score = 87
grade = calculate_grade(student_score)
print(f"Student score {student_score} = Grade {grade}")

Output:

Testing calculate_grade...
  ✓ A grades: passed
  ✓ B grades: passed
  ✓ C grades: passed
  ✓ D grades: passed
  ✓ F grades: passed
  ✓ Boundary cases: passed
All tests passed! ✓
 
Student score 87 = Grade B

Manfaat pendekatan ini:

  1. Organisasi test yang jelas: Anda bisa melihat semua test case sekilas
  2. Mudah dijalankan: Cukup panggil test_calculate_grade() kapan pun Anda mengubah fungsi
  3. Umpan balik progresif: Anda bisa melihat grup test mana yang lulus saat fungsi berjalan
  4. Self-documenting: Fungsi test menunjukkan persis bagaimana calculate_grade() seharusnya bekerja

Kapan menjalankan test Anda:

  • Sebelum membuat perubahan: Pastikan test Anda lulus pada kode saat ini
  • Setelah membuat perubahan: Verifikasi Anda tidak merusak apa pun
  • Saat menambahkan fitur: Tulis test untuk fitur baru terlebih dahulu (test-driven development)
  • Saat memperbaiki bug: Tambahkan test yang mereproduksi bug, lalu perbaiki

Pola sederhana ini—menulis fungsi pengujian dengan assertion—adalah fondasi dari pengujian software profesional. Seiring Anda maju, Anda akan belajar tentang framework pengujian seperti pytest dan unittest, tetapi ide utamanya tetap sama: tulis fungsi yang memverifikasi kode Anda bekerja dengan benar.

41.5.6) Kapan Menggunakan Assertions vs Exceptions

Memahami kapan harus menggunakan assertion versus exception itu krusial. Keduanya punya tujuan yang fundamentally berbeda:

Assertion digunakan untuk menemukan bug selama development:

  • Mereka mengecek hal-hal yang seharusnya tidak pernah false jika kode Anda ditulis dengan benar
  • Mereka memverifikasi asumsi internal dan logika dari kode Anda sendiri
  • Mereka membantu Anda menangkap kesalahan pemrograman saat Anda menulis dan menguji kode
  • Contoh: "Pada titik ini dalam fungsi saya, list ini seharusnya tidak pernah kosong"
  • Contoh: "Semua item di list ini seharusnya integer karena saya baru saja memfilternya"

Exception digunakan untuk menangani error yang bisa terjadi saat operasi normal:

  • Mereka menangani kondisi eksternal yang tidak bisa Anda kontrol
  • Mereka menangani situasi yang mungkin terjadi bahkan ketika kode Anda sempurna
  • Mereka memungkinkan program Anda pulih dengan elegan atau gagal dengan informasi yang jelas
  • Contoh: Pengguna memasukkan teks padahal Anda mengharapkan angka
  • Contoh: File yang kode Anda coba buka tidak ada
  • Contoh: Network request timeout

Perbedaan kuncinya: Assertion berkata "ini seharusnya mustahil," sementara exception berkata "ini mungkin terjadi, dan begini cara kita menanganinya."

Mari lihat praktiknya:

python
# Contoh 1: Fungsi digunakan dengan INPUT PENGGUNA
# Pengguna bisa memasukkan apa saja, termasuk 0
def calculate_user_ratio(numerator, denominator):
    """Menghitung rasio dari angka yang diberikan pengguna."""
    # Pengguna bisa memasukkan 0, jadi gunakan penanganan exception
    if denominator == 0:
        raise ValueError("Denominator cannot be zero")
    
    return numerator / denominator
 
# Contoh 2: Perhitungan internal di mana 0 seharusnya mustahil
def calculate_percentage(part, total):
    """Menghitung berapa persen 'part' dari 'total'."""
    # Ini dipanggil secara internal setelah kita memverifikasi total > 0
    # Jika total adalah 0, itu bug pemrograman di kode kita
    assert total > 0, "total must be positive - check calling code"
    
    return (part / total) * 100

Contoh lain tentang apa yang seharusnya ditangani masing-masing:

SituationUse AssertionUse Exception
User enters invalid input❌ No✅ Yes
File doesn't exist❌ No✅ Yes
Network request fails❌ No✅ Yes
Function gets wrong parameter type from your code✅ Yes❌ No
List should have items but is empty due to logic error✅ Yes❌ No
Data structure in unexpected state due to bug✅ Yes❌ No
Database connection fails❌ No✅ Yes
API returns unexpected format❌ No✅ Yes
Your algorithm produces mathematically impossible result✅ Yes❌ No

Keterbatasan kritis assertion:

Assertion bisa dinonaktifkan sepenuhnya saat Python berjalan dengan optimisasi:

bash
python -O script.py  # All assert statements are ignored!

Saat assertion dinonaktifkan, mereka benar-benar hilang—Python tidak mengeceknya sama sekali. Ini berarti:

  • Jangan pernah gunakan assertion untuk memvalidasi input pengguna
  • Jangan pernah gunakan assertion untuk pemeriksaan keamanan
  • Jangan pernah gunakan assertion untuk apa pun yang harus selalu bekerja di produksi
python
# BERBAHAYA - JANGAN LAKUKAN INI:
def process_payment(amount):
    assert amount > 0, "Amount must be positive"  # SALAH! Dinonaktifkan dengan -O
    # Process payment...
 
# BENAR - LAKUKAN INI:
def process_payment(amount):
    if amount <= 0:
        raise ValueError("Amount must be positive")  # Selalu dicek!
    # Process payment...

Ringkasnya:

  • Assertion = "Saya mengecek kode saya sendiri untuk bug selama development"

    • Pikirkan: "Ini seharusnya mustahil jika saya ngoding dengan benar"
    • Mereka membantu Anda menemukan kesalahan dalam logika
  • Exception = "Saya menangani kondisi dunia nyata yang memang bisa terjadi"

    • Pikirkan: "Ini bisa terjadi saat penggunaan normal, dan saya perlu menanganinya"
    • Mereka membantu program Anda menangani situasi yang tidak terduga

Assertion adalah alat development dan debugging yang membantu Anda menulis kode yang benar. Exception adalah alat produksi yang membantu program Anda menangani realitas yang berantakan dari input pengguna, file system, network, dan faktor eksternal lainnya yang tidak bisa Anda kontrol.


Anda sekarang sudah mempelajari teknik debugging dan pengujian penting yang akan berguna sepanjang perjalanan pemrograman Anda:

  • Membaca tracebacks untuk cepat menemukan di mana error terjadi
  • Menelusuri kode secara mental untuk memahami apa yang dilakukan kode Anda langkah demi langkah
  • Menggunakan print statement secara strategis untuk melihat nilai dan alur saat runtime
  • Menginspeksi objek dengan type() dan dir() untuk memahami apa yang sedang Anda gunakan
  • Menguji dengan assertions untuk memverifikasi kode Anda bekerja dan menangkap bug lebih awal

Keterampilan ini bekerja bersama sebagai toolkit debugging yang lengkap. Saat Anda menemui masalah:

  1. Baca traceback untuk menemukan di mana ia gagal
  2. Gunakan print debugging atau penelusuran mental untuk memahami kenapa
  3. Gunakan inspeksi type/dir ketika Anda tidak yakin apa yang bisa dilakukan sebuah objek
  4. Tulis assertion untuk mencegah bug itu kembali

Dengan latihan, Anda akan mengembangkan intuisi tentang teknik mana yang dipakai di setiap situasi. Ingat: setiap programmer melakukan debugging—bedanya adalah programmer berpengalaman melakukannya secara sistematis dan efisien. Teknik-teknik ini akan membuat Anda jadi salah satunya.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai