Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Polymorphism?
  5. 2. One Variable, Many Objects
  6. 3. A List of the Parent Type
  7. 4. The Object Decides — Dynamic Dispatch
  8. 5. Functions That Accept the Parent Type
  9. 6. Polymorphism Through Interfaces
  10. 7. Reaching Subtype-Only Members with is
  11. 8. Why Needing is Is a Warning Sign
  12. 9. Replacing Type Checks with Overridden Methods
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 65 — Polymorphism

Category10 — Advanced OOP
Lecture065 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

A separate list and loop for every kind of object does not scale. With polymorphism, one variable, one list, one function handles every subtype — each running its own method.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 059–064:

  • extends, overriding with @override, and super
  • abstract classes and abstract methods
  • interfaces with implements, and mixins with with
  • lists and for-in loops (Lectures 027, 039)
  • the type test is and the cast as (Lecture 010)

Connection With Previous Lectures

In Lecture 060, Animal pet = Dog(); followed by pet.speak() printed Woof! — the object, not the variable, decided which version ran.

That was one object. A real program has many: circles, squares and triangles; full-time staff and freelancers. A separate list and loop for each kind means more code to change with every new kind. We want to treat them all as one group and still get the right behaviour from each.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what polymorphism means
  • Store objects of different subclasses in one parent-typed variable or list
  • Explain dynamic dispatch: the object, not the variable, picks the method
  • Write functions that accept the parent type and work with every subtype
  • Reach subtype-only members with is and type promotion
  • Recognize type checks as a design smell
  • Replace an if/switch on types with an overridden method

1. What Is Polymorphism?

Polymorphism means "many forms". One piece of code talks to an object through a general type — Animal, Shape — and the object behaves in its own specific way.

Analogy — a teacher says "Everyone, introduce yourselves." One sentence, but Ali, Sara and Zayan each answer differently. The teacher does not need a separate instruction for each student.


2. One Variable, Many Objects

A variable of the parent type can hold any subclass object, and can switch to another one later:

dart
class Animal {
  void speak() => print('...');
}

class Dog extends Animal {
  @override
  void speak() => print('Woof!');
}

class Cat extends Animal {
  @override
  void speak() => print('Meow!');
}

void main() {
  Animal pet = Dog();
  pet.speak();
  pet = Cat();
  pet.speak();
}
output
Woof!
Meow!

The same line, pet.speak(), gave two results because the object inside pet changed.


3. A List of the Parent Type

A List<Shape> accepts every kind of shape, and one loop handles them all:

dart
abstract class Shape {
  double get area;
}

class Circle extends Shape {
  Circle(this.radius);
  final double radius;

  @override
  double get area => 3.14 * radius * radius;
}

class Square extends Shape {
  Square(this.side);
  final double side;

  @override
  double get area => side * side;
}

void main() {
  List<Shape> shapes = [Circle(1), Square(2), Square(3)];
  double total = 0;
  for (var shape in shapes) {
    total += shape.area;
  }
  print('Total area: $total');
}
output
Total area: 16.14

The loop never asks what kind of shape it has; each object computes its own area.


4. The Object Decides — Dynamic Dispatch

Before running, Dart checks the variable's type: does Animal have a speak()? While running, it asks the object which speak() to use. This run-time choice is called dynamic dispatch. Here, reading the code cannot tell you the answer:

dart
class Animal {
  void speak() => print('...');
}

class Dog extends Animal {
  @override
  void speak() => print('Woof!');
}

class Cat extends Animal {
  @override
  void speak() => print('Meow!');
}

Animal adopt(int ticket) => ticket.isEven ? Dog() : Cat();

void main() {
  for (var ticket in [1, 2, 3]) {
    adopt(ticket).speak();
  }
}
output
Meow!
Woof!
Meow!

adopt promises only an Animal. Which speak() runs is decided each time, by the object.


5. Functions That Accept the Parent Type

A parameter of the parent type accepts every subtype — even ones not written yet:

dart
abstract class Shape {
  double get area;
}

class Square extends Shape {
  Square(this.side);
  final double side;

  @override
  double get area => side * side;
}

class Triangle extends Shape {
  Triangle(this.base, this.height);
  final double base, height;

  @override
  double get area => base * height / 2;
}

void describe(Shape shape) {
  print('${shape.runtimeType} has area ${shape.area}');
}

void main() {
  describe(Square(3));
  describe(Triangle(4, 5));
}
output
Square has area 9.0
Triangle has area 10.0

Triangle was added after describe was written, and describe did not change at all.


6. Polymorphism Through Interfaces

The shared type can also be an interface (Lecture 063). Unrelated classes that implement it share one list:

dart
abstract class Payable {
  double get amountDue;
}

class Employee implements Payable {
  Employee(this.salary);
  final double salary;

  @override
  double get amountDue => salary;
}

class Invoice implements Payable {
  Invoice(this.total);
  final double total;

  @override
  double get amountDue => total;
}

void main() {
  List<Payable> bills = [Employee(50000), Invoice(3600)];
  for (var bill in bills) {
    print(bill.amountDue);
  }
}
output
50000.0
3600.0

An employee is not an invoice, but both can be paid — that is all the loop needs. A mixin type (Lecture 064) works the same way.


7. Reaching Subtype-Only Members with is

Through a parent-typed variable, Dart allows only the parent's members:

dart
class Animal {}

class Dog extends Animal {
  void fetch() => print('Fetching!');
}

void main() {
  Animal pet = Dog();
  pet.fetch();
}
output
Error: The method 'fetch' isn't defined for the type 'Animal'.
Try correcting the name to the name of an existing method, or defining a method named 'fetch'.
  pet.fetch();
      ^^^^^

An is check promotes the variable: inside the if, Dart treats pet as a Dog (Lecture 059):

dart
class Animal {}

class Dog extends Animal {
  void fetch() => print('Fetching!');
}

void main() {
  Animal pet = Dog();
  if (pet is Dog) {
    pet.fetch();
  }
}
output
Fetching!

8. Why Needing is Is a Warning Sign

is works, but a chain of type checks usually means the classes are not doing their job:

dart
abstract class Shape {}

class Circle extends Shape {
  Circle(this.radius);
  final double radius;
}

class Square extends Shape {
  Square(this.side);
  final double side;
}

class Triangle extends Shape {}

double areaOf(Shape shape) {
  if (shape is Circle) {
    return 3.14 * shape.radius * shape.radius;
  } else if (shape is Square) {
    return shape.side * shape.side;
  }
  return 0;
}

void main() {
  print(areaOf(Square(2)));
  print(areaOf(Triangle()));
}
output
4.0
0.0

Someone added Triangle but forgot areaOf — no error, just a wrong answer. Every new shape means hunting down every chain like this one.


9. Replacing Type Checks with Overridden Methods

Move each branch into its own class as an override, and the chain disappears:

dart
abstract class Shape {
  double get area;
}

class Circle extends Shape {
  Circle(this.radius);
  final double radius;

  @override
  double get area => 3.14 * radius * radius;
}

class Square extends Shape {
  Square(this.side);
  final double side;

  @override
  double get area => side * side;
}

void main() {
  for (Shape shape in [Circle(1), Square(2)]) {
    print(shape.area);
  }
}
output
3.14
4.0

A new Triangle now must provide area — forgetting it is a compile error (Lecture 062), not a silent 0.0. The same applies to a switch on runtimeType: ask the object, don't inspect it.

Dart also has sealed classes, which let a switch on types be checked for missing cases. We will study them in Lecture 082 — Sealed Classes.


10. A Real-World Example

A shop sells items priced in different ways. The cart treats them all as CartItem:

dart
abstract class CartItem {
  CartItem(this.name);
  final String name;

  double get price;
}

class FixedItem extends CartItem {
  FixedItem(super.name, this.cost);
  final double cost;

  @override
  double get price => cost;
}

class WeighedItem extends CartItem {
  WeighedItem(super.name, this.kilos, this.pricePerKilo);
  final double kilos, pricePerKilo;

  @override
  double get price => kilos * pricePerKilo;
}

double checkout(List<CartItem> cart) {
  double total = 0;
  for (var item in cart) {
    print('${item.name}: ${item.price}');
    total += item.price;
  }
  return total;
}

void main() {
  var cart = [
    FixedItem('Notebook', 500),
    WeighedItem('Apples', 2, 300),
    FixedItem('Bag', 750),
  ];
  print('Total: ${checkout(cart)}');
}
output
Notebook: 500.0
Apples: 600.0
Bag: 750.0
Total: 1850.0

checkout reads price without knowing the item's kind; each class answers for itself.


Code Examples

Example 1 — Parent Variable, Child Object

dart
class Vehicle {
  void move() => print('Moving');
}

class Bike extends Vehicle {
  @override
  void move() => print('Pedalling');
}

void main() {
  Vehicle v = Bike();
  v.move();
}
output
Pedalling

Example 2 — Reassigning the Variable

dart
class Light {
  String get colour => 'white';
}

class RedLight extends Light {
  @override
  String get colour => 'red';
}

void main() {
  Light light = Light();
  print(light.colour);
  light = RedLight();
  print(light.colour);
}
output
white
red

Example 3 — A Mixed List

dart
abstract class Instrument {
  void play();
}

class Drum extends Instrument {
  @override
  void play() => print('Boom');
}

class Flute extends Instrument {
  @override
  void play() => print('Toot');
}

void main() {
  for (var i in [Drum(), Flute(), Drum()]) {
    i.play();
  }
}
output
Boom
Toot
Boom

Example 4 — An Inherited Version Still Counts

dart
class Bird {
  String get move => 'flies';
}

class Penguin extends Bird {
  @override
  String get move => 'swims';
}

class Sparrow extends Bird {}

void main() {
  List<Bird> birds = [Penguin(), Sparrow()];
  for (var b in birds) {
    print('${b.runtimeType} ${b.move}');
  }
}
output
Penguin swims
Sparrow flies

Sparrow does not override, so it uses the inherited version.

Example 5 — A Function with a Parent Parameter

dart
abstract class Greeter {
  String greet(String name);
}

class English extends Greeter {
  @override
  String greet(String name) => 'Hello, $name';
}

class Urdu extends Greeter {
  @override
  String greet(String name) => 'Assalam-o-Alaikum, $name';
}

void welcome(Greeter g) => print(g.greet('Zayan'));

void main() {
  welcome(English());
  welcome(Urdu());
}
output
Hello, Zayan
Assalam-o-Alaikum, Zayan

Example 6 — Through an Interface

dart
abstract class Printable {
  String describe();
}

class Book implements Printable {
  @override
  String describe() => 'Book: Dart Basics';
}

class Photo implements Printable {
  @override
  String describe() => 'Photo: Beach';
}

void main() {
  List<Printable> queue = [Book(), Photo()];
  for (var item in queue) {
    print(item.describe());
  }
}
output
Book: Dart Basics
Photo: Beach

Example 7 — Student Marks

dart
abstract class Student {
  Student(this.marks);
  final int marks;

  bool get passed;
}

class Regular extends Student {
  Regular(super.marks);

  @override
  bool get passed => marks >= 50;
}

class Honors extends Student {
  Honors(super.marks);

  @override
  bool get passed => marks >= 75;
}

void main() {
  for (Student s in [Regular(60), Honors(70)]) {
    print('${s.runtimeType} ${s.marks}: ${s.passed ? 'Pass' : 'Fail'}');
  }
}
output
Regular 60: Pass
Honors 70: Fail

Example 8 — Shopping Discounts

dart
abstract class Discount {
  double apply(double total);
}

class NoDiscount extends Discount {
  @override
  double apply(double total) => total;
}

class PercentOff extends Discount {
  PercentOff(this.percent);
  final double percent;

  @override
  double apply(double total) => total - total * percent / 100;
}

void main() {
  for (var d in [NoDiscount(), PercentOff(10)]) {
    print(d.apply(2000));
  }
}
output
2000.0
1800.0

Example 9 — Bank Accounts and Monthly Fees

dart
abstract class Account {
  double balance = 20000;

  double get monthlyFee;

  void chargeFee() => balance -= monthlyFee;
}

class BasicAccount extends Account {
  @override
  double get monthlyFee => 100;
}

class PremiumAccount extends Account {
  @override
  double get monthlyFee => balance > 10000 ? 0 : 250;
}

void main() {
  for (Account a in [BasicAccount(), PremiumAccount()]) {
    a.chargeFee();
    print('${a.runtimeType}: ${a.balance}');
  }
}
output
BasicAccount: 19900.0
PremiumAccount: 20000.0

chargeFee lives in the parent, but monthlyFee comes from each object.

Example 10 — Temperature Sensors

dart
abstract class Sensor {
  Sensor(this.reading);
  final double reading;

  double get celsius;
}

class CelsiusSensor extends Sensor {
  CelsiusSensor(super.reading);

  @override
  double get celsius => reading;
}

class FahrenheitSensor extends Sensor {
  FahrenheitSensor(super.reading);

  @override
  double get celsius => (reading - 32) * 5 / 9;
}

void main() {
  List<Sensor> sensors = [CelsiusSensor(25), FahrenheitSensor(86)];
  for (var s in sensors) {
    print('${s.celsius} °C');
  }
}
output
25.0 °C
30.0 °C

Common Beginner Mistakes

1. Calling a subtype-only member through the parent type

dart
void main() {
  Animal pet = Dog();
  // pet.fetch(); // ✗
  if (pet is Dog) pet.fetch(); // ✓
}
output
Error: The method 'fetch' isn't defined for the type 'Animal'.

Needing this often means the method belongs in the parent (section 8).

2. Putting a parent object into a child variable

dart
void main() {
  // Dog d = Animal(); // ✗ not every animal is a dog
  Animal a = Dog(); // ✓ every dog is an animal
}
output
Error: A value of type 'Animal' can't be assigned to a variable of type 'Dog'.

3. Forcing the type with as

dart
void main() {
  Animal pet = Cat();
  // (pet as Dog).fetch(); // ✗ crashes while running
}
output
Unhandled exception:
type 'Cat' is not a subtype of type 'Dog' in type cast

as does not check first. Use is when you are not sure.


Best Practices

  • Type variables, lists, and parameters with the most general type that fits.
  • Put behaviour that differs by kind in an overridden method, not in if/switch on types.
  • Make the parent abstract when it has no sensible default, so every child must override.
  • Use an interface as the shared type when the classes are not related by "is-a".
  • Treat is checks on your own classes as a hint to redesign; avoid as unless certain.

Mini Challenge

Create an abstract class Employee with a name and an abstract getter pay. FullTime earns a fixed salary; PartTime earns hours * rate. Write void payroll(List<Employee> staff) that prints each employee's pay and then the total.

Your program should print exactly:

output
Ali: 60000.0
Sara: 24000.0
Total: 84000.0

Try it yourself before opening the solution.

Show solution
dart
abstract class Employee {
  Employee(this.name);
  final String name;

  double get pay;
}

class FullTime extends Employee {
  FullTime(super.name, this.salary);
  final double salary;

  @override
  double get pay => salary;
}

class PartTime extends Employee {
  PartTime(super.name, this.hours, this.rate);
  final double hours, rate;

  @override
  double get pay => hours * rate;
}

void payroll(List<Employee> staff) {
  double total = 0;
  for (var e in staff) {
    print('${e.name}: ${e.pay}');
    total += e.pay;
  }
  print('Total: $total');
}

void main() {
  payroll([FullTime('Ali', 60000), PartTime('Sara', 80, 300)]);
}
output
Ali: 60000.0
Sara: 24000.0
Total: 84000.0

payroll never checks the employee's kind — each object knows its own pay.


Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Create Animal with speak() printing ..., and Cow overriding it to print Moo. Store a Cow in an Animal variable and call speak().
  2. Create a List<Animal> holding a Cow, a plain Animal, and another Cow. Call speak() on each in a loop.
  3. Does Dog d = Animal(); compile? Explain in one sentence.

Application

  1. Create abstract Shape with a getter area, and Rectangle(width, height) and Circle(radius) (use 3.14). Print the total area of Rectangle(2, 3) and Circle(1).
  2. Write void announce(Vehicle v) that prints v.sound. Car returns 'Beep', Train returns 'Choo choo'. Call it with both.
  3. Create an interface Chargeable with int get batteryLevel. Phone and Laptop implement it with 80 and 45. Print the levels from a List<Chargeable>.
  4. Animal has no fetch(), but Dog does. In a loop over [Dog(), Cat()], call fetch() only on dogs, using is.

Challenge

  1. Rewrite this function so that it has no is checks, by moving the logic into the classes:

    dart
    String soundOf(Animal a) {
      if (a is Dog) return 'Woof';
      if (a is Cat) return 'Meow';
      return '...';
    }
  2. Bird has a getter canFly returning true. Ostrich overrides it to return false; Eagle does not override. Loop over [Eagle(), Ostrich()] and print, for example, Eagle: true.

  3. Predict the output, then explain it in one sentence:

    dart
    class A {
      String get name => 'A';
      void show() => print('I am $name');
    }
    
    class B extends A {
      @override
      String get name => 'B';
    }
    
    void main() {
      A x = B();
      x.show();
    }

Practice Answers

Question 1 — Solution

dart
class Animal {
  void speak() => print('...');
}

class Cow extends Animal {
  @override
  void speak() => print('Moo');
}

void main() {
  Animal a = Cow();
  a.speak();
}
output
Moo

Question 2 — Solution

dart
class Animal {
  void speak() => print('...');
}

class Cow extends Animal {
  @override
  void speak() => print('Moo');
}

void main() {
  List<Animal> animals = [Cow(), Animal(), Cow()];
  for (var a in animals) {
    a.speak();
  }
}
output
Moo
...
Moo

Question 3 — Solution

No — not every animal is a dog: A value of type 'Animal' can't be assigned to a variable of type 'Dog'.

Question 4 — Solution

dart
abstract class Shape {
  double get area;
}

class Rectangle extends Shape {
  Rectangle(this.width, this.height);
  final double width, height;

  @override
  double get area => width * height;
}

class Circle extends Shape {
  Circle(this.radius);
  final double radius;

  @override
  double get area => 3.14 * radius * radius;
}

void main() {
  List<Shape> shapes = [Rectangle(2, 3), Circle(1)];
  double total = 0;
  for (var s in shapes) {
    total += s.area;
  }
  print(total);
}
output
9.14

Question 5 — Solution

dart
abstract class Vehicle {
  String get sound;
}

class Car extends Vehicle {
  @override
  String get sound => 'Beep';
}

class Train extends Vehicle {
  @override
  String get sound => 'Choo choo';
}

void announce(Vehicle v) => print(v.sound);

void main() {
  announce(Car());
  announce(Train());
}
output
Beep
Choo choo

Question 6 — Solution

dart
abstract class Chargeable {
  int get batteryLevel;
}

class Phone implements Chargeable {
  @override
  int get batteryLevel => 80;
}

class Laptop implements Chargeable {
  @override
  int get batteryLevel => 45;
}

void main() {
  List<Chargeable> devices = [Phone(), Laptop()];
  for (var d in devices) {
    print(d.batteryLevel);
  }
}
output
80
45

Question 7 — Solution

dart
class Animal {}

class Dog extends Animal {
  void fetch() => print('Fetching!');
}

class Cat extends Animal {}

void main() {
  for (Animal a in [Dog(), Cat()]) {
    if (a is Dog) {
      a.fetch();
    }
  }
}
output
Fetching!

Question 8 — Solution

dart
class Animal {
  String get sound => '...';
}

class Dog extends Animal {
  @override
  String get sound => 'Woof';
}

class Cat extends Animal {
  @override
  String get sound => 'Meow';
}

void main() {
  for (var a in [Dog(), Cat(), Animal()]) {
    print(a.sound);
  }
}
output
Woof
Meow
...

Question 9 — Solution

dart
class Bird {
  bool get canFly => true;
}

class Eagle extends Bird {}

class Ostrich extends Bird {
  @override
  bool get canFly => false;
}

void main() {
  for (Bird b in [Eagle(), Ostrich()]) {
    print('${b.runtimeType}: ${b.canFly}');
  }
}
output
Eagle: true
Ostrich: false

Question 10 — Solution

output
I am B

show() is written in A, but name is looked up on the object, a B — dynamic dispatch works even inside the parent's own methods.


Quick Revision

  1. What does "polymorphism" mean?
  2. Which types of objects can a List<Shape> hold?
  3. Who decides which override runs — the variable's type or the object?
  4. Why should a function's parameter use the parent type?
  5. What does an is check do to a variable inside the if?
  6. What replaces an if/switch on types, and why?

Key Takeaways

  • Polymorphism: one general type, many specific behaviours; a parent-typed variable or list holds any subtype object.
  • Dynamic dispatch: Dart checks the variable's type before running, but the object chooses the method while running.
  • Functions that accept the parent type work with every subtype, even future ones.
  • An interface gives unrelated classes a shared type.
  • is promotes a variable to reach subtype-only members — but needing it often is a design smell.
  • Replace if/switch on types with an overridden method; an abstract parent makes sure no subtype forgets it.

What's Next?

Lecture 66 — Composition vs Inheritance. Polymorphism makes inheritance look powerful — but not every relationship is "is-a". A car is not an engine; it has one. Next we will learn composition, building objects out of other objects, and how to choose between it and inheritance.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.