Grade by Grade
A. Programming Concepts and Skills · Data Types, Files and Expressions

Type Conversion in an Inheritance Hierarchy

A1.2 A1.5About 65 minutes
Learning goals
  • I can read a simple inheritance hierarchy and explain the "is-a" relationship.
  • I can explain upcasting and downcasting, and predict when a Java cast compiles, runs or throws ClassCastException.
  • I can use isinstance, issubclass and type in Python to process a list of mixed objects safely.
  • I can explain why Python uses type checks and duck typing instead of casts.

Words to know

inheritance
Defining a new class (subclass) from an existing one (superclass), so it gets all the superclass's fields and methods.
is-a relationship
A subclass object is also an object of its superclass: a Bus is a Vehicle.
upcasting
Treating a subclass object as its superclass type. Always safe; automatic in Java.
downcasting
Treating a superclass reference as a subclass type. Only safe if the object really is that subclass; needs an explicit cast in Java.
`ClassCastException`
The Java run-time error thrown when a downcast is attempted on an object of the wrong class.
`isinstance(obj, C)`
True if obj is an instance of class C or any subclass of C.
duck typing
Python's style of using an object by what it can do (its methods) rather than by checking its exact class.

When is a bus a vehicle?

A city transit system runs buses, trains and share bikes. Some questions apply to every vehicle (How many passengers can it carry?). Others apply only to some (What route is this bus on?). Programs model this with inheritance: a general class Vehicle, and more specific classes Bus, Train and Bike that extend it.

The curriculum asks you to understand type conversion in an inheritance hierarchy, also called casting. When can a Bus be treated as a Vehicle? When can a Vehicle be treated as a Bus? Java answers with casts; Python answers with run-time checks. You will learn both, because university courses often use Java. (How to design hierarchies, override methods and use polymorphism is the job of the sibling unit Modular and Object-Oriented Design.)

objectVehicleBusTrainBikeevery class is a kind of objectfields: ident, capacity+ route+ cars+ dock
An inheritance hierarchy. Arrows point from a subclass to its superclass: a Bus is a Vehicle, and every class is an object. Subclasses inherit ident and capacity and add their own fields.

Writing a small hierarchy in Python

  • class Bus(Vehicle): means Bus inherits from Vehicle.
  • Inside Bus.__init__, super().__init__(ident, capacity) runs Vehicle's initializer so the shared fields are set up, then Bus adds its own route.
  • Every Bus object therefore has ident, capacity and route. A Train has ident, capacity and cars.

Because every subclass object is a Vehicle, a single list can hold all of them. Any code that only uses Vehicle fields works on every element.

class Vehicle:
"""Any vehicle in the transit fleet."""
def __init__(self, ident, capacity):
self.ident = ident
self.capacity = capacity # passengers
class Bus(Vehicle):
"""A bus that runs on a numbered route."""
def __init__(self, ident, capacity, route):
super().__init__(ident, capacity)
self.route = route
class Train(Vehicle):
"""A train made of several cars."""
def __init__(self, ident, capacity, cars):
super().__init__(ident, capacity)
self.cars = cars
class Bike(Vehicle):
"""A share bike parked at a dock."""
def __init__(self, ident, dock):
super().__init__(ident, 1)
self.dock = dock
fleet = [Bus("B-101", 70, 44), Train("T-7", 1100, 6), Bike("K-12", "Main St"), Bus("B-205", 55, 9)]
print(sum(v.capacity for v in fleet)) # works for every Vehicle
for v in fleet:
if isinstance(v, Bus): # safe "downcast"
print(v.ident, "route", v.route)
print(isinstance(fleet[1], Vehicle), type(fleet[1]) is Vehicle)
print(issubclass(Bike, Vehicle), issubclass(Vehicle, Bike))

A list whose declared element type is the superclass. Code that only needs Vehicle fields works on every element. Code that needs route first checks isinstance(v, Bus). type(x) is C is stricter: it ignores subclasses. Output, line by line: 1226 | B-101 route 44 | B-205 route 9 | True False | True False

VehicleBusupcastalways safe, automaticdowncastonly if it really is a Bus:check first (isinstance)Java: Vehicle v = bus; Bus b = (Bus) v;
Casting in a hierarchy. Treating a Bus as a Vehicle (upcasting) is always safe. Treating a Vehicle as a Bus (downcasting) is only safe when the object really is a Bus, so check with instanceof in Java or isinstance in Python first.

Upcasting and downcasting

  • Upcasting (subclass → superclass) only forgets details. A Bus treated as a Vehicle still is a Bus; the code just promises not to ask about its route. It is always safe, and Java does it automatically: Vehicle v = new Bus(...);.
  • Downcasting (superclass → subclass) assumes details. A Vehicle reference might point to a Bus, a Train or a Bike. Java makes you write the cast (Bus) v, and checks it when the program runs. If the object is not a Bus, Java throws ClassCastException.

The safe pattern in both languages: check, then use. Java: if (v instanceof Bus) { Bus b = (Bus) v; ... } (newer Java also allows if (v instanceof Bus b)). Python: if isinstance(v, Bus): ....

Worked example

Example 1: Upcasting and downcasting in Java

Given class Bus extends Vehicle, decide which Java lines compile, which run safely, and which throw an exception:

  1. Vehicle v = new Bus("B-101", 70, 44);
  2. int r = v.route;
  3. Bus b = (Bus) v;
  4. Vehicle w = new Train("T-7", 1100, 6);
  5. Bus c = (Bus) w;
  1. Line 1: upcast, automatic and safe. A Bus is a Vehicle.
  2. Line 2: does not compile. The variable's declared type is Vehicle, and Vehicle has no route field. Java checks fields against the declared type, not the object.
  3. Line 3: explicit downcast. It compiles, and runs safely because the object really is a Bus.
  4. Line 4: upcast of a Train, safe.
  5. Line 5: compiles (a Vehicle might be a Bus), but at run time the object is a Train, so Java throws ClassCastException. Safe version: if (w instanceof Bus) { Bus c = (Bus) w; ... }.

Lines 1, 3 and 4 work. Line 2 is a compile error. Line 5 throws ClassCastException.

Worked example

Example 2: The same program in Python

Rewrite the safe parts of Example 1 in Python and explain why Python has no cast operator.

  1. Python variables have no declared type, so there is nothing to cast to: v = Bus("B-101", 70, 44) simply refers to a Bus object.
  2. Python decides whether v.route works by looking at the object when the line runs. On a Bus it works; on a Train it raises AttributeError.
  3. The Python version of a safe downcast is a type check: if isinstance(v, Bus): print(v.route).
  4. Many Python programmers prefer duck typing instead: rather than asking "is it a Bus?", give every vehicle a method such as describe() and let each class answer in its own way. That idea, polymorphism, is the heart of the sibling unit Modular and Object-Oriented Design.
  5. Type hints such as def board(v: Vehicle) -> None: document the expected type for readers and checking tools, but Python does not enforce them when the program runs.

if isinstance(v, Bus): print(v.route). Python checks the object's real type at run time, so it needs checks rather than casts.

class Vehicle:
"""Any vehicle in the transit fleet."""
def __init__(self, ident, capacity):
self.ident = ident
self.capacity = capacity # passengers
class Bus(Vehicle):
"""A bus that runs on a numbered route."""
def __init__(self, ident, capacity, route):
super().__init__(ident, capacity)
self.route = route
class Train(Vehicle):
"""A train made of several cars."""
def __init__(self, ident, capacity, cars):
super().__init__(ident, capacity)
self.cars = cars
class Bike(Vehicle):
"""A share bike parked at a dock."""
def __init__(self, ident, dock):
super().__init__(ident, 1)
self.dock = dock
v = Train("T-7", 1100, 6)
try:
print(v.route)
except AttributeError as e:
print("AttributeError:", e)
print(isinstance(True, int), True + 1, Bus.__mro__[1].__name__)

Without a check, using a subclass-only field on the wrong kind of object fails when that line runs. (Java would throw ClassCastException at the cast instead.) Python's own types form hierarchies too: bool is a subclass of int. Output, line by line: AttributeError: 'Train' object has no attribute 'route' | True 2 Vehicle

isinstance or type?

isinstance(x, C) accepts subclasses, which is almost always what you want: a CityBus(Bus) added next year will still be treated as a Bus. type(x) is C rejects subclasses and breaks that kind of extension, so use it only when you truly need the exact class. Remember that isinstance(True, int) is True, because bool is a subclass of int: test for bool before int if the difference matters.

Just for fun

Why did the Train get a ClassCastException at the bus stop?

It tried to pass itself off as a Bus, but Java checked its ticket at run time.

Hands-on activity

Classify the vehicles on your street

You need: a window or a short walk (if you go out, stay on the sidewalk and keep watching for traffic), paper, and Python.

  1. For 10 minutes, record every vehicle you see: cars, buses, bikes, trucks, snowplows, delivery vans. Note one special detail for some of them (a bus route number, a truck's company, a bike's colour).
  2. Draw a class hierarchy on paper with Vehicle at the top and at least three subclasses. Put shared fields in Vehicle and special fields in the subclasses.
  3. Code the hierarchy and make one list holding an object for every vehicle you saw.
  4. Print the total count, then use isinstance to print each bus route and each truck's company. Show what error you get if you ask a bike for its route without checking.

What to look for: shared fields in the superclass, super().__init__ in each subclass, isinstance checks before subclass-only fields, and the AttributeError explained.

Your turn

1

In the fleet example, is isinstance(Bike("K-1", "Elm"), Vehicle) True or False? What about type(...) is Vehicle?

2

What is issubclass(Vehicle, Train)?

3

Which direction of casting is always safe?

4

In Java, Vehicle v = new Bike(...); Bus b = (Bus) v; What happens?

5

Write Python that prints the number of cars of every train in fleet.

6

What capacity does every Bike get, and where is that set?

7

What is isinstance(False, int)? Why could that cause a bug in a function that treats ints and bools differently?

More practice: warm-up

1

What keyword-like call runs the superclass's __init__?

2

A Bus is a Vehicle. Is a Vehicle always a Bus?

3

Which Python function checks whether an object belongs to a class or its subclasses?

4

What exception does Java throw for a bad downcast?

5

What does Python raise if you read a field an object does not have?

More practice: on your own

1

Add a subclass ExpressBus(Bus) with a field stops (a list). Is an ExpressBus counted by isinstance(v, Bus)? By isinstance(v, Vehicle)?

2

Rewrite this so it works for ExpressBus too: if type(v) is Bus: print(v.route).

3

Explain why line 2 of Example 1 is a compile-time error in Java, while the matching Python code fails only at run time.

4

Write a function total_seats(fleet, kind) that adds the capacity of only the vehicles of the given class, for example total_seats(fleet, Bus).

5

A function needs to call v.describe() on each vehicle, but only Bus and Train have that method. Give two ways to avoid an AttributeError.

Challenge

1

Print ExpressBus.__mro__ for the class from the previous set. What does it show, and how does isinstance use it?

2

Lots of isinstance checks spread through a program are often called a "code smell". Explain the maintenance problem and the object-oriented alternative.

3

In Java, can you cast a Bus to a Train? Will it compile? Explain using the hierarchy.

Check your understanding

Quiz

1.

In the hierarchy Vehicle → Bus, which statement is true?

2.

Which Java statement throws ClassCastException at run time?

3.

What does isinstance(b, Vehicle) return when b is a Bus?

4.

Why does Python have no cast operator like (Bus) v?

5.

What is isinstance(True, int)?

6.

Which check still works after someone adds class CityBus(Bus)?

For parents and teachers

Big idea: A1.2 includes "casting in an inheritance hierarchy". In Java this means upcasting and downcasting; in Python it means checking an object's real type before using subclass-only features. Lists of mixed objects continue A1.5. Full object-oriented design is taught in the unit Modular and Object-Oriented Design.

How to help: the street-watching activity can be done from a window. Ask your teen: "Is every bus a vehicle? Is every vehicle a bus? What goes wrong if a program assumes so?"

Common difficulties:

  • Mixing up the directions: upcasting is the safe one.
  • Forgetting super().__init__ in a subclass, so the shared fields never get set.
  • Using type(x) is C where isinstance is needed.
  • Expecting Python type hints to stop the program when the wrong type is passed. They do not.