Skip to content

OOP Basics - Find the Bug Exercises

Each exercise contains buggy code. Find the bug, explain why it's wrong, and fix it.


Bug 1: Forgot to Call super()

Python — Buggy Code

class Animal:
    def __init__(self, name: str):
        self.name = name
        self.sound = "..."

class Dog(Animal):
    def __init__(self, name: str, breed: str):
        self.breed = breed  # BUG: Where is super().__init__()?

    def speak(self) -> str:
        return f"{self.name} says Woof!"  # AttributeError: 'Dog' has no attribute 'name'

dog = Dog("Rex", "Labrador")
print(dog.speak())

Java — Buggy Code

class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        // BUG: Missing super(name) call
        // Java requires calling super() if parent has no default constructor
        this.breed = breed;
    }
    // Compiler error: constructor Animal() not found
}

Fix

Python:

class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name)   # FIX: Call parent initializer
        self.breed = breed

Java:

class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name);            // FIX: Call parent constructor
        this.breed = breed;
    }
}


Bug 2: Wrong Access Modifier — Broken Encapsulation

Java — Buggy Code

public class BankAccount {
    public double balance;  // BUG: balance is public!

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
        }
    }

    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            return true;
        }
        return false;
    }
}

// Anyone can do this:
BankAccount acc = new BankAccount(1000);
acc.balance = -999999;  // Bypasses all validation!

Go — Buggy Code

type BankAccount struct {
    Owner   string
    Balance float64  // BUG: Exported field, anyone can modify directly
}

func (a *BankAccount) Deposit(amount float64) {
    if amount > 0 {
        a.Balance += amount
    }
}

// From another package:
// acc.Balance = -999999  // Bypasses validation!

Fix

Java:

public class BankAccount {
    private double balance;  // FIX: make private

    public double getBalance() {
        return balance;      // Provide read-only access
    }
    // deposit() and withdraw() are the only way to change balance
}

Go:

type BankAccount struct {
    owner   string
    balance float64  // FIX: unexported (lowercase)
}

func (a *BankAccount) Balance() float64 {
    return a.balance  // Provide getter
}


Bug 3: Interface Not Satisfied (Go)

Go — Buggy Code

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

// BUG: Only Area() is implemented, Perimeter() is missing
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func PrintShape(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
    c := Circle{Radius: 5}
    PrintShape(c)
    // Compiler error: Circle does not implement Shape
    // (missing method Perimeter)
}

Fix

// FIX: Implement ALL methods required by the interface
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius  // FIX: Add missing method
}

Bug 4: Wrong Method Resolution (Python MRO)

Python — Buggy Code

class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):
    pass

d = D()
print(d.method())  # Developer expects "C" but gets "B"

# BUG: Developer doesn't understand MRO
# MRO of D is: D → B → C → A
# B comes before C, so B.method() is called

Fix

# If you want C's behavior, either:

# Option 1: Change inheritance order
class D(C, B):  # Now MRO is D → C → B → A
    pass

# Option 2: Explicitly call the desired method
class D(B, C):
    def method(self):
        return C.method(self)  # Explicitly call C's version

# Option 3: Use super() chain properly
class B(A):
    def method(self):
        return "B+" + super().method()

class C(A):
    def method(self):
        return "C+" + super().method()

class D(B, C):
    def method(self):
        return super().method()

d = D()
print(d.method())  # "B+C+A" — follows MRO chain

Bug 5: Shallow Copy of Mutable Field

Python — Buggy Code

class Team:
    def __init__(self, name: str, members: list[str] = []):  # BUG: Mutable default!
        self.name = name
        self.members = members

team1 = Team("Alpha")
team1.members.append("Alice")

team2 = Team("Beta")
team2.members.append("Bob")

print(team1.members)  # Expected: ["Alice"], Actual: ["Alice", "Bob"]!
print(team2.members)  # Expected: ["Bob"],   Actual: ["Alice", "Bob"]!
# Both teams share the SAME list object!

Java — Buggy Code

import java.util.*;

public class Team {
    private String name;
    private List<String> members;

    public Team(String name, List<String> members) {
        this.name = name;
        this.members = members;  // BUG: Stores reference, not copy!
    }

    public List<String> getMembers() {
        return members;  // BUG: Returns mutable reference!
    }
}

List<String> names = new ArrayList<>(List.of("Alice", "Bob"));
Team team = new Team("Alpha", names);
names.add("Eve");           // Modifies the team's internal list!
team.getMembers().clear();  // Also modifies internal state!

Go — Buggy Code

type Team struct {
    Name    string
    Members []string
}

func NewTeam(name string, members []string) *Team {
    return &Team{
        Name:    name,
        Members: members, // BUG: Stores the same slice header
    }
}

members := []string{"Alice", "Bob"}
team := NewTeam("Alpha", members)
members[0] = "CHANGED"
fmt.Println(team.Members[0]) // "CHANGED" — shared underlying array!

Fix

Python:

class Team:
    def __init__(self, name: str, members: list[str] | None = None):
        self.name = name
        self.members = list(members) if members else []  # FIX: Create a copy

Java:

public class Team {
    private final String name;
    private final List<String> members;

    public Team(String name, List<String> members) {
        this.name = name;
        this.members = new ArrayList<>(members);  // FIX: Defensive copy
    }

    public List<String> getMembers() {
        return Collections.unmodifiableList(members);  // FIX: Immutable view
    }
}

Go:

func NewTeam(name string, members []string) *Team {
    copied := make([]string, len(members))
    copy(copied, members)
    return &Team{
        Name:    name,
        Members: copied, // FIX: Copy the slice
    }
}


Bug 6: Circular Dependency

Go — Buggy Code

// package user
package user

import "myapp/order" // BUG: user imports order

type User struct {
    Name   string
    Orders []*order.Order
}

// package order
package order

import "myapp/user" // BUG: order imports user → CIRCULAR!

type Order struct {
    ID     string
    Buyer  *user.User
}

// Compiler error: import cycle not allowed

Fix

// FIX: Extract shared types into a separate package

// package models (shared types)
package models

type User struct {
    Name   string
    Orders []*Order
}

type Order struct {
    ID    string
    Buyer *User
}

// OR use interfaces to break the cycle:

// package user
type OrderInfo interface {
    GetID() string
}

type User struct {
    Name   string
    Orders []OrderInfo  // Depends on interface, not concrete type
}

// package order
type BuyerInfo interface {
    GetName() string
}

type Order struct {
    ID    string
    Buyer BuyerInfo  // Depends on interface, not concrete type
}

Bug 7: Missing Method Implementation (Java)

Java — Buggy Code

interface Drawable {
    void draw();
    void resize(double factor);
}

abstract class Shape implements Drawable {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    @Override
    public void draw() {
        System.out.println("Drawing " + color + " shape");
    }
    // Note: resize() is NOT implemented here — that's OK for abstract class
}

class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    // BUG: Circle doesn't implement resize()!
    // Shape left it abstract, Circle must implement it
    // Compiler error: Circle is not abstract and does not override
    // abstract method resize(double) in Drawable
}

Fix

class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public void resize(double factor) {  // FIX: Implement the missing method
        this.radius *= factor;
    }

    @Override
    public void draw() {
        System.out.println("Drawing " + color + " circle with radius " + radius);
    }
}

Bug 8: Pointer Receiver vs Value Receiver (Go)

Go — Buggy Code

type Counter struct {
    count int
}

// BUG: Value receiver — modifies a COPY, not the original
func (c Counter) Increment() {
    c.count++
}

func (c Counter) GetCount() int {
    return c.count
}

func main() {
    c := Counter{}
    c.Increment()
    c.Increment()
    c.Increment()
    fmt.Println(c.GetCount()) // Expected: 3, Actual: 0!
}

Fix

// FIX: Use pointer receiver for methods that modify state
func (c *Counter) Increment() {
    c.count++
}

func (c Counter) GetCount() int { // Value receiver is OK for read-only
    return c.count
}

func main() {
    c := Counter{}
    c.Increment()
    c.Increment()
    c.Increment()
    fmt.Println(c.GetCount()) // Now correctly prints: 3
}

Bug 9: __eq__ Without __hash__ (Python)

Python — Buggy Code

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __eq__(self, other) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    # BUG: Defined __eq__ but not __hash__!
    # Python sets __hash__ = None when you define __eq__

p1 = Point(1, 2)
p2 = Point(1, 2)

print(p1 == p2)  # True — works fine

# But:
my_set = {p1}
# TypeError: unhashable type: 'Point'
# OR in older Python, unexpected behavior in dicts/sets

my_dict = {}
my_dict[p1] = "hello"
# TypeError: unhashable type: 'Point'

Fix

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __eq__(self, other) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:  # FIX: Always define __hash__ with __eq__
        return hash((self.x, self.y))

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)         # True
print({p1, p2})         # {Point(1, 2)} — one element (they're equal)
print(len({p1, p2}))    # 1

Bug 10: Singleton Not Thread-Safe (Java)

Java — Buggy Code

public class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {
        System.out.println("Creating connection...");
        // Simulate slow initialization
        try { Thread.sleep(100); } catch (InterruptedException e) {}
    }

    // BUG: Not thread-safe!
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
            // Race condition: two threads can both see instance == null
            // and create two instances
        }
        return instance;
    }
}

// Thread 1: getInstance() → sees null → starts creating
// Thread 2: getInstance() → sees null → also starts creating
// Result: Two instances created!

Fix

public class DatabaseConnection {
    // FIX Option 1: Double-checked locking
    private static volatile DatabaseConnection instance;

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {  // Double-check inside synchronized
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }

    // FIX Option 2 (recommended): Initialization-on-demand holder
    // private static class Holder {
    //     static final DatabaseConnection INSTANCE = new DatabaseConnection();
    // }
    // public static DatabaseConnection getInstance() {
    //     return Holder.INSTANCE;
    // }

    // FIX Option 3: Use enum (simplest)
    // public enum DatabaseConnection {
    //     INSTANCE;
    // }
}

Bug 11: Comparing Objects with == Instead of .equals() (Java)

Java — Buggy Code

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

User u1 = new User("Alice", 25);
User u2 = new User("Alice", 25);

// BUG: == compares references, not values!
if (u1 == u2) {
    System.out.println("Same user");
} else {
    System.out.println("Different users"); // This prints!
}

// Also buggy with strings:
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);  // false! (different objects)

Fix

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {  // FIX: Override equals
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User other = (User) obj;
        return age == other.age && Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {  // FIX: Always override hashCode with equals
        return Objects.hash(name, age);
    }
}

User u1 = new User("Alice", 25);
User u2 = new User("Alice", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1 == u2);      // still false (different objects)

Bug 12: Goroutine Data Race on Shared Struct (Go)

Go — Buggy Code

type Counter struct {
    value int
}

func (c *Counter) Increment() {
    c.value++ // BUG: Not safe for concurrent access!
}

func (c *Counter) Value() int {
    return c.value
}

func main() {
    c := &Counter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment() // DATA RACE!
        }()
    }

    wg.Wait()
    fmt.Println(c.Value()) // Expected: 1000, Actual: some random number < 1000
}

Fix

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()         // FIX: Lock before modifying
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

// Alternative FIX: Use atomic operations
type AtomicCounter struct {
    value atomic.Int64
}

func (c *AtomicCounter) Increment() {
    c.value.Add(1)
}

func (c *AtomicCounter) Value() int64 {
    return c.value.Load()
}

Summary of Common OOP Bugs

# Bug Type Languages Root Cause
1 Missing super().__init__() Py, Java Forgetting to initialize parent class
2 Public mutable fields All Breaking encapsulation
3 Unimplemented interface methods Go Partial interface implementation
4 Wrong MRO assumptions Python Not understanding C3 linearization
5 Shallow copy of mutable field All Sharing references to mutable objects
6 Circular dependency All Tight coupling between packages/modules
7 Missing abstract method impl Java Forgetting to implement inherited abstract
8 Value vs pointer receiver Go Modifying a copy instead of the original
9 __eq__ without __hash__ Python Breaking set/dict contract
10 Non-thread-safe singleton Java Race condition in lazy initialization
11 == instead of .equals() Java Reference comparison vs value comparison
12 Data race on shared struct Go Concurrent access without synchronization