Skip to Content
BonicBot A2DevelopmentPython ProgrammingLesson 16 - Object-Oriented Programming

Lesson 16 - Object-Oriented Programming

Learning Objective

  • Define a class with __init__ and methods.
  • Extend a class through inheritance.
  • Build objects out of other objects through composition.

Introduction

You’ve been using objects since Lesson 1 without necessarily naming them as such — every time you wrote:

with BonicBot(host=...) as bot:

you created an instance of the BonicBot class.

A class is a blueprint; an object (or instance) is one specific thing built from that blueprint, with its own data.

This lesson has three parts:

  1. Writing a class from scratch.
  2. Extending a class through inheritance (an “is-a” relationship).
  3. Building a class out of other objects through composition (a “has-a” relationship).

Both inheritance and composition are patterns that bonicbot_bridge itself uses throughout its real code.


Part A — Classes and Objects

Code

classes_basics.py
class Student: def __init__(self, name, scores): self.name = name self.scores = scores def average(self): return sum(self.scores) / len(self.scores) def summary(self): return f"{self.name}: average {self.average():.1f}" aditi = Student("Aditi", [82, 91, 76]) rahul = Student("Rahul", [70, 65, 80]) print(aditi.summary()) print(rahul.summary()) students = [aditi, rahul] for student in students: print(student.name, student.average())

Expected Output

Click to see expected output

Aditi: average 83.0 Rahul: average 71.7 Aditi 83.0 Rahul 71.66666666666667

🔧 Under the Hood

What is self, really?

self is simply the specific object a method was called on.

When you write:

aditi.average()

Python actually performs something equivalent to:

Student.average(aditi)

behind the scenes.

Inside the method, self refers to aditi, which is why:

self.scores

reads:

[82, 91, 76]

for aditi, but:

[70, 65, 80]

for rahul.

Each instance has its own copy of the attributes assigned in __init__, even though all instances share the same class definition.


Part B — Inheritance (“is-a”)

A subclass inherits everything from its parent class and can override any piece of it.

Code

inheritance_basics.py
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound." class Dog(Animal): def speak(self): return f"{self.name} says Woof!" class Cat(Animal): def speak(self): return f"{self.name} says Meow!" animals = [ Dog("Rex"), Cat("Whiskers"), Animal("Generic Creature") ] for animal in animals: print(animal.speak()) print(isinstance(Dog("Rex"), Animal))

Expected Output

Click to see expected output

Rex says Woof! Whiskers says Meow! Generic Creature makes a sound. True

🔧 You’ve Already Used Inheritance — in Lesson 14

How inheritance appeared in the SDK

Dog never defines its own __init__, yet this still works:

Dog("Rex")

because it automatically inherits Animal.__init__().

It only overrides:

speak()

replacing the parent’s implementation for that subclass.

This is why:

isinstance(Dog("Rex"), Animal)

returns True.

A Dog is an Animal.

Open exceptions.py in bonicbot_bridge and you’ll see the exact same pattern:

class ConnectionError(BonicBotError): pass class ServoError(BonicBotError): pass

Every custom exception inherits from BonicBotError.

That means:

except BonicBotError:

would catch:

  • ConnectionError
  • ServoError
  • and every other SDK-specific exception.

In Lesson 14 you caught ConnectionError specifically, but the inheritance hierarchy was there the whole time.


Part C — Composition (“has-a”)

Composition means an object holds other objects as attributes rather than inheriting from them.

Code

composition_basics.py
class Engine: def __init__(self, horsepower): self.horsepower = horsepower def start(self): return f"Engine starting ({self.horsepower} HP)." class Car: def __init__(self, make, horsepower): self.make = make self.engine = Engine(horsepower) def start(self): return f"{self.make}: {self.engine.start()}" my_car = Car("Tata", 120) print(my_car.start()) print(my_car.engine.horsepower)

Expected Output

Click to see expected output

Tata: Engine starting (120 HP). 120

🔧 BonicBot Bridge Is Built Entirely Out of Composition

How composition powers the SDK

Car doesn’t inherit from Engine.

A car is not an engine.

A car has an engine:

self.engine = Engine(horsepower)

That’s composition.

This is exactly how core.py builds BonicBot.

Inside connect() you’ll find code like:

self.motion = MotionController(self.ros) self.servo = ServoController(self.ros) self.sensors = SensorManager(self.ros)

The robot:

  • has a motion controller,
  • has a servo controller,
  • has a sensor manager.

Every shortcut method you’ve used, such as:

bot.move_forward(...)

is really just delegating to:

self.motion.move_forward(...)

Many methods in core.py literally state:

Delegates to: …

because that’s exactly what’s happening.


Student Challenge

Challenge 1 — Python Only

Combine both OOP patterns into a single working program:

  • Create a base class Shape with an area(self) method that returns 0.
  • Create two subclasses that inherit from Shape (Inheritance):
    • Circle: takes radius in __init__ and overrides area() to return $\pi \times r^2$ (math.pi * self.radius ** 2).
    • Rectangle: takes width and height in __init__ and overrides area() to return width * height.
  • Create a Drawing class that manages shapes (Composition):
    • In __init__, initialize an empty list self.shapes = [].
    • Add an add(self, shape) method to append a shape instance to self.shapes.
    • Add a total_area(self) method that loops through all stored shapes and returns their combined area.
  • Instantiate a Drawing object, add a Circle(3) and a Rectangle(4, 5), iterate through drawing.shapes to print each shape’s type (type(shape).__name__) and individual area, and print the calculated total area.

Hint

type(shape).__name__ reveals the actual subclass (Circle or Rectangle) even though every object is stored uniformly inside drawing.shapes.

Each object still runs its own overridden version of area().

Click to see solution

shapes.py
import math class Shape: def area(self): return 0 class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2 class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height class Drawing: def __init__(self): self.shapes = [] def add(self, shape): self.shapes.append(shape) def total_area(self): total = 0 for shape in self.shapes: total += shape.area() return total drawing = Drawing() drawing.add(Circle(3)) drawing.add(Rectangle(4, 5)) for shape in drawing.shapes: print(f"{type(shape).__name__}: {shape.area():.2f}") print(f"Total area: {drawing.total_area():.2f}")

Challenge 2 — Robot

Inspect how composition is used inside the bonicbot_bridge SDK by examining your BonicBot instance directly.

  • Connect to your robot using with BonicBot(host='192.168.0.188') as bot:.
  • Print the class name of the main bot instance using type(bot).__name__.
  • Print the class names of its composed controller attributes: type(bot.motion).__name__, type(bot.servo).__name__, and type(bot.sensors).__name__.
  • Compare direct shortcut method execution with composed object method execution:
    • Call bot.move_forward(speed=0.2, duration=1.0)
    • Call bot.motion.move_forward(speed=0.2, duration=1.0)
  • Observe that both calls invoke the exact same movement behavior because top-level methods on bot delegate directly to bot.motion.

Hint

Both calls execute the same underlying code:

bot.get_servo_angles()

and

bot.servo.get_servo_angles()

because core.py defines the shortcut as:

return self.servo.get_servo_angles()

Click to see solution

challenge_inspect_composition.py
from bonicbot_bridge import BonicBot with BonicBot(host='192.168.0.188') as bot: print(type(bot).__name__) print(type(bot.motion).__name__) print(type(bot.servo).__name__) print(type(bot.sensors).__name__) bot.move_forward(speed=0.2, duration=1.0) bot.motion.move_forward( speed=0.2, duration=1.0 ) bot.stop()

Challenge 3 — Robot (Inheritance in Practice)

Practice inheritance by creating a custom exception subclass that extends the SDK’s built-in error hierarchy.

  • Define a custom exception LowBatteryError that inherits from BonicBotError (imported from bonicbot_bridge.exceptions).
  • Write a safety check function require_battery(bot, minimum_percent=30) that gets the current battery level with bot.get_battery(). If the battery level is lower than minimum_percent, raise LowBatteryError(...) with an explanatory message.
  • In a with BonicBot(...) as bot: block, call require_battery(bot, minimum_percent=30) inside a try/except block:
    • If the battery check succeeds, print a success message and drive forward 0.3 meters.
    • Add an except LowBatteryError as exc: block to handle low battery gracefully by printing an abortion notice.
    • Add an except BonicBotError as exc: block as a fallback catch-all for any other SDK exceptions.
  • Test your code by raising minimum_percent to a high value like 90 to confirm your custom LowBatteryError is raised and caught properly.

Hint

LowBatteryError is a subclass you wrote, but it’s still a BonicBotError.

That’s why:

except BonicBotError:

would catch it even without:

except LowBatteryError:

appearing first.

Click to see solution

challenge_custom_exception.py
from bonicbot_bridge import BonicBot from bonicbot_bridge.exceptions import BonicBotError class LowBatteryError(BonicBotError): """Raised when the battery is too low to safely start a task.""" pass def require_battery(bot, minimum_percent=30): battery = bot.get_battery() if battery < minimum_percent: raise LowBatteryError( f"Battery at {battery}% — " f"need at least {minimum_percent}%." ) return battery with BonicBot(host='192.168.0.188') as bot: try: battery = require_battery( bot, minimum_percent=30 ) print( f"Battery OK at {battery}% — " f"starting patrol." ) bot.drive_distance( 0.3, speed=0.2 ) except LowBatteryError as exc: print(f"Patrol aborted: {exc}") except BonicBotError as exc: print( f"Some other robot error occurred: " f"{exc}" )

Change minimum_percent to something high, such as 90, and confirm that LowBatteryError is raised instead of the patrol running.


OOP Quick Reference

Classes

ConceptPurpose
class Name:Define a class
__init__()Constructor called during object creation
self.attributeStore data on an object
object.method()Call a method
isinstance(obj, Class)Check object type

Reflection Questions

bot.move_forward() and bot.motion.move_forward() do the exact same thing. Given what you now know about composition, why do you think core.py bothers defining move_forward() as a shortcut at all, instead of requiring everyone to always write bot.motion.move_forward()?

LowBatteryError inherits from BonicBotError — the same base class every built-in bonicbot_bridge exception uses. What did inheriting from BonicBotError give you for free that inheriting directly from Python’s plain Exception would not?

Last updated on