Episode 4: Many Bricks, One Action (Polymorphism)
Welcome Back, Master Builder! ๐๏ธ
Remember in Episode 3 when we learned about inheritance and how bricks can be part of a family tree? Well, buckle up because today weโre diving into polymorphism - which is just a fancy Greek word meaning โmany forms.โ
Think about it: Every LEGO brick can connect to another brick. Whether itโs a tiny 1x1, a massive baseplate, a Technic gear, or even a minifigureโs head - they all click together. But they each do it in their own unique way! Thatโs polymorphism in a nutshell.
The beauty? You donโt need to know WHAT kind of brick youโre connecting - you just know it CAN connect. Itโs like the universal language of LEGO studs and tubes! ๐งฑโจ
The Universal Language of LEGO Studs ๐
Imagine youโre at a massive LEGO convention. People from all over the world are building together. Nobody speaks the same language, but everyone understands one thing: click means connect.
class StandardBrick:
"""A standard 2x4 LEGO brick"""
def __init__(self, color):
self.color = color
def connect(self):
return f"*Standard CLICK!* ๐ {self.color} brick connected"
class TechnicBrick:
"""A LEGO Technic brick with holes"""
def __init__(self, color):
self.color = color
def connect(self):
return f"*CLICK-SNAP!* โ๏ธ {self.color} Technic brick locked in with axle!"
class Baseplate:
"""A massive LEGO baseplate"""
def __init__(self, color):
self.color = color
def connect(self):
return f"*HEAVY THUD!* ๐๏ธ {self.color} baseplate providing foundation!"
class MinifigureHead:
"""A minifigure head (yes, it connects too!)"""
def __init__(self, expression):
self.expression = expression
def connect(self):
return f"*gentle click* ๐ {self.expression} head attached to torso!"
# The magic of polymorphism - one function, many types!
def attach_brick(brick):
"""Works with ANY brick that has a connect() method!"""
print(brick.connect())
# Let's test with different brick types
standard = StandardBrick("red")
technic = TechnicBrick("black")
base = Baseplate("green")
head = MinifigureHead("happy")
# Same function call - different behaviors!
attach_brick(standard) # *Standard CLICK!* ๐ red brick connected
attach_brick(technic) # *CLICK-SNAP!* โ๏ธ black Technic brick locked in with axle!
attach_brick(base) # *HEAVY THUD!* ๐๏ธ green baseplate providing foundation!
attach_brick(head) # *gentle click* ๐ happy head attached to torso!
What just happened?! ๐คฏ
The attach_brick() function doesnโt care WHAT type of brick it receives. It just knows that whatever comes in has a connect() method. Thatโs polymorphism! Same interface, different implementations.
Duck Typing: If It Looks Like a LEGOโฆ ๐ฆ
Python has something called โduck typing.โ The name comes from the saying:
โIf it walks like a duck and quacks like a duck, then it must be a duck.โ
In LEGO terms:
โIf it has studs like a LEGO and clicks like a LEGO, then it must be a LEGO!โ
Python doesnโt check if something IS a certain type - it checks if it CAN DO what youโre asking.
class OfficialLEGOBrick:
"""An official LEGO brick"""
def __init__(self, color):
self.color = color
self.manufacturer = "LEGO Group"
def connect(self):
return f"Official {self.color} LEGO brick - connected! โ
"
def make_sound(self):
return "*satisfying LEGO click* ๐"
class OffBrandBrick:
"""A non-LEGO brick (gasp!) but it still works!"""
def __init__(self, color):
self.color = color
self.manufacturer = "Definitely Not LEGO Inc."
def connect(self):
return f"Off-brand {self.color} brick - connected (kinda)! โ ๏ธ"
def make_sound(self):
return "*slightly less satisfying click* ๐"
class WoodenBlock:
"""A wooden building block - totally different!"""
def __init__(self, color):
self.color = color
def connect(self):
return f"{self.color} wooden block - just stacking, no studs! ๐ชต"
def make_sound(self):
return "*clunk* ๐ชต"
def build_tower(bricks):
"""Build a tower with ANY bricks that can connect!"""
tower = []
for brick in bricks:
tower.append(brick.connect())
print(brick.make_sound())
return tower
# Mix and match - Python doesn't care!
my_bricks = [
OfficialLEGOBrick("red"),
OffBrandBrick("blue"),
WoodenBlock("yellow"),
OfficialLEGOBrick("green")
]
tower = build_tower(my_bricks)
# *satisfying LEGO click* ๐
# *slightly less satisfying click* ๐
# *clunk* ๐ชต
# *satisfying LEGO click* ๐
# They all connected! Duck typing in action! ๐ฆ
Notice how build_tower() works with ALL these objects, even though theyโre completely different types? Thatโs the power of duck typing. If it has a connect() method and a make_sound() method, it works!
Method Overriding: Same Name, Different Game ๐ฎ
Remember inheritance from Episode 3? When child classes override parent methods, thatโs polymorphism too! Same method name, but each class does its own thing.
class Vehicle:
"""Base LEGO vehicle class"""
def __init__(self, color):
self.color = color
def move(self):
return f"{self.color} vehicle is moving..."
def make_noise(self):
return "vrrrm" # Generic vehicle noise
class Car(Vehicle):
"""LEGO car - overrides movement"""
def move(self):
return f"๐ {self.color} car is driving on roads!"
def make_noise(self):
return "VROOM VROOM! ๐๏ธ"
class Boat(Vehicle):
"""LEGO boat - different movement entirely!"""
def move(self):
return f"โต {self.color} boat is sailing on water!"
def make_noise(self):
return "TOOT TOOT! ๐ฏ"
class Helicopter(Vehicle):
"""LEGO helicopter - flying high!"""
def move(self):
return f"๐ {self.color} helicopter is flying in the air!"
def make_noise(self):
return "THWOP THWOP THWOP! ๐"
class Submarine(Vehicle):
"""LEGO submarine - underwater adventure!"""
def move(self):
return f"๐คฟ {self.color} submarine is diving underwater!"
def make_noise(self):
return "*silent running* ๐ *PING* ๐ก"
# Polymorphic function - works with ALL vehicles!
def start_chase_scene(vehicles):
"""Epic LEGO movie chase scene!"""
print("๐ฌ ACTION! ๐ฌ\n")
for vehicle in vehicles:
print(vehicle.make_noise())
print(vehicle.move())
print()
# Assemble the cast!
fleet = [
Car("red"),
Boat("blue"),
Helicopter("white"),
Submarine("yellow"),
Car("black") # The villain's car!
]
start_chase_scene(fleet)
# ๐ฌ ACTION! ๐ฌ
#
# VROOM VROOM! ๐๏ธ
# ๐ red car is driving on roads!
#
# TOOT TOOT! ๐ฏ
# โต blue boat is sailing on water!
#
# THWOP THWOP THWOP! ๐
# ๐ white helicopter is flying in the air!
#
# *silent running* ๐ *PING* ๐ก
# ๐คฟ yellow submarine is diving underwater!
#
# VROOM VROOM! ๐๏ธ
# ๐ black car is driving on roads!
Same method names (move() and make_noise()), completely different behaviors! Thatโs polymorphism making your code clean and flexible. You can add new vehicle types without changing start_chase_scene()!
Operator Overloading: When Bricks Do Math! โ
Hereโs where it gets REALLY fun. In Python, you can make your LEGO bricks work with operators like +, -, *, and even ==. Itโs like teaching your bricks to do arithmetic!
class BrickSet:
"""A collection of LEGO bricks"""
def __init__(self, brick_count, color="mixed"):
self.brick_count = brick_count
self.color = color
def __add__(self, other):
"""Override the + operator - combine brick sets!"""
total_bricks = self.brick_count + other.brick_count
return BrickSet(total_bricks, "combined")
def __sub__(self, other):
"""Override the - operator - remove some bricks!"""
remaining = max(0, self.brick_count - other.brick_count)
return BrickSet(remaining, self.color)
def __mul__(self, multiplier):
"""Override the * operator - multiply brick sets!"""
return BrickSet(self.brick_count * multiplier, self.color)
def __eq__(self, other):
"""Override the == operator - are sets equal?"""
return self.brick_count == other.brick_count
def __lt__(self, other):
"""Override the < operator - is this set smaller?"""
return self.brick_count < other.brick_count
def __str__(self):
"""Override str() - readable description"""
return f"{self.brick_count} {self.color} bricks"
def __repr__(self):
"""Override repr() - technical description"""
return f"BrickSet(brick_count={self.brick_count}, color='{self.color}')"
def __len__(self):
"""Override len() - get brick count"""
return self.brick_count
# Let's do some brick math! ๐งฎ
small_set = BrickSet(50, "red")
medium_set = BrickSet(100, "blue")
large_set = BrickSet(200, "yellow")
# Addition - combine sets!
combined = small_set + medium_set
print(combined) # 150 combined bricks
# Subtraction - remove some bricks!
remaining = large_set - small_set
print(remaining) # 150 yellow bricks
# Multiplication - buy multiple sets!
bulk_order = small_set * 5
print(bulk_order) # 250 red bricks
# Comparison - which is bigger?
print(small_set < medium_set) # True
print(small_set == BrickSet(50, "green")) # True (same count!)
# Length - how many bricks?
print(len(large_set)) # 200
# String representation
print(f"I have {small_set} and {medium_set}")
# I have 50 red bricks and 100 blue bricks
Magic Methods Cheat Sheet:
| Operator | Method | Example |
|---|---|---|
+ |
__add__ |
set1 + set2 |
- |
__sub__ |
set1 - set2 |
* |
__mul__ |
set1 * 3 |
/ |
__truediv__ |
set1 / 2 |
== |
__eq__ |
set1 == set2 |
< |
__lt__ |
set1 < set2 |
> |
__gt__ |
set1 > set2 |
len() |
__len__ |
len(set1) |
str() |
__str__ |
str(set1) |
repr() |
__repr__ |
repr(set1) |
Real-World Example: The Multi-Cloud LEGO Factory ๐ญ
Letโs build something practical! Imagine youโre deploying LEGO factories to different cloud providers (Azure, AWS, GCP). Each cloud works differently, but you want ONE deployment script.
class CloudProvider:
"""Base cloud provider - polymorphic interface"""
def __init__(self, name):
self.name = name
self.deployments = []
def deploy_factory(self, factory_name):
"""Each cloud deploys differently!"""
raise NotImplementedError("Subclass must implement deploy_factory()")
def get_cost(self, hours):
"""Each cloud prices differently!"""
raise NotImplementedError("Subclass must implement get_cost()")
def __str__(self):
return f"{self.name} Cloud Provider"
class AzureCloud(CloudProvider):
"""Microsoft Azure - LEGO factories on Azure!"""
def __init__(self):
super().__init__("Azure")
self.rate_per_hour = 10.0
def deploy_factory(self, factory_name):
deployment = f"๐ต Deploying {factory_name} to Azure West Europe"
self.deployments.append(deployment)
return deployment
def get_cost(self, hours):
return f"โฌ{self.rate_per_hour * hours:.2f} (Azure pricing)"
class AWSCloud(CloudProvider):
"""Amazon AWS - LEGO factories on AWS!"""
def __init__(self):
super().__init__("AWS")
self.rate_per_hour = 9.50
def deploy_factory(self, factory_name):
deployment = f"๐ง Deploying {factory_name} to AWS eu-west-1"
self.deployments.append(deployment)
return deployment
def get_cost(self, hours):
return f"โฌ{self.rate_per_hour * hours:.2f} (AWS pricing)"
class GCPCloud(CloudProvider):
"""Google Cloud Platform - LEGO factories on GCP!"""
def __init__(self):
super().__init__("GCP")
self.rate_per_hour = 9.00
def deploy_factory(self, factory_name):
deployment = f"๐ด Deploying {factory_name} to GCP europe-west4"
self.deployments.append(deployment)
return deployment
def get_cost(self, hours):
return f"โฌ{self.rate_per_hour * hours:.2f} (GCP pricing + sustained use discount)"
class MultiCloudDeployer:
"""
Polymorphic deployer - works with ANY cloud provider!
This is the power of polymorphism in action! ๐
"""
def __init__(self, providers):
self.providers = providers
def deploy_to_all_clouds(self, factory_name):
"""Deploy to multiple clouds - polymorphism makes this easy!"""
print(f"๐ญ Deploying {factory_name} to all clouds...\n")
for provider in self.providers:
# Same method call - different behavior per cloud!
result = provider.deploy_factory(factory_name)
print(result)
print("\nโ
Multi-cloud deployment complete!")
def cost_comparison(self, hours):
"""Compare costs across clouds"""
print(f"\n๐ฐ Cost comparison for {hours} hours:\n")
for provider in self.providers:
# Same method call - different pricing per cloud!
cost = provider.get_cost(hours)
print(f"{provider.name}: {cost}")
def find_cheapest(self, hours):
"""Find the cheapest cloud - polymorphism + algorithms!"""
costs = {}
for provider in self.providers:
# Extract numeric cost (hacky but demonstrates polymorphism)
cost_str = provider.get_cost(hours)
cost = float(cost_str.split('โฌ')[1].split()[0])
costs[provider.name] = cost
cheapest = min(costs, key=costs.get)
return f"๐ Cheapest option: {cheapest} at โฌ{costs[cheapest]:.2f}"
# Assemble your multi-cloud fleet!
azure = AzureCloud()
aws = AWSCloud()
gcp = GCPCloud()
# Create multi-cloud deployer
deployer = MultiCloudDeployer([azure, aws, gcp])
# Deploy to all clouds with ONE command!
deployer.deploy_to_all_clouds("LEGO-Factory-Europe")
# ๐ญ Deploying LEGO-Factory-Europe to all clouds...
#
# ๐ต Deploying LEGO-Factory-Europe to Azure West Europe
# ๐ง Deploying LEGO-Factory-Europe to AWS eu-west-1
# ๐ด Deploying LEGO-Factory-Europe to GCP europe-west4
#
# โ
Multi-cloud deployment complete!
# Compare costs
deployer.cost_comparison(720) # 30 days
# ๐ฐ Cost comparison for 720 hours:
#
# Azure: โฌ7200.00 (Azure pricing)
# AWS: โฌ6840.00 (AWS pricing)
# GCP: โฌ6480.00 (GCP pricing + sustained use discount)
# Find the best deal
print(deployer.find_cheapest(720))
# ๐ Cheapest option: GCP at โฌ6480.00
The Magic Here:
- ONE
MultiCloudDeployerclass works with ALL cloud providers - Adding a new cloud provider? Just create a new class with
deploy_factory()andget_cost() - No changes needed to
MultiCloudDeployer! - Thatโs polymorphism making your code flexible and maintainable! ๐ฏ
The LEGO Sorting Machine: Polymorphism in Action ๐
Letโs build a LEGO sorting machine that can handle ANY type of brick!
class SortableBrick:
"""Base class for sortable items"""
def get_sort_key(self):
"""Each brick type defines how it should be sorted"""
raise NotImplementedError
class ColorBrick(SortableBrick):
"""Sort by color"""
def __init__(self, color, size):
self.color = color
self.size = size
def get_sort_key(self):
return self.color
def __str__(self):
return f"{self.color} {self.size}x{self.size} brick"
class SizeBrick(SortableBrick):
"""Sort by size"""
def __init__(self, color, size):
self.color = color
self.size = size
def get_sort_key(self):
return self.size
def __str__(self):
return f"{self.size}x{self.size} {self.color} brick"
class SpecialBrick(SortableBrick):
"""Sort by special category"""
def __init__(self, category, name):
self.category = category
self.name = name
def get_sort_key(self):
return self.category
def __str__(self):
return f"{self.name} ({self.category})"
class BrickSorter:
"""Polymorphic sorter - works with ANY SortableBrick!"""
def __init__(self, bricks):
self.bricks = bricks
def sort_bricks(self):
"""Sort using each brick's own sorting logic"""
return sorted(self.bricks, key=lambda b: b.get_sort_key())
def group_by_key(self):
"""Group bricks by their sort key"""
groups = {}
for brick in self.bricks:
key = brick.get_sort_key()
if key not in groups:
groups[key] = []
groups[key].append(brick)
return groups
# Create a chaotic pile of bricks!
pile = [
ColorBrick("red", 2),
ColorBrick("blue", 4),
SizeBrick("yellow", 1),
SizeBrick("green", 2),
SpecialBrick("technic", "Gear wheel"),
SpecialBrick("wheels", "Race car wheel"),
ColorBrick("red", 4),
SpecialBrick("technic", "Axle"),
]
# Sort them!
sorter = BrickSorter(pile)
print("๐ Sorted bricks:")
for brick in sorter.sort_bricks():
print(f" โข {brick}")
print("\n๐ฆ Grouped by sort key:")
groups = sorter.group_by_key()
for key, bricks in groups.items():
print(f"\n{key.upper()}:")
for brick in bricks:
print(f" โข {brick}")
Why This Works:
Each brick class decides HOW it wants to be sorted (by color, size, or category), but the BrickSorter doesnโt care! It just calls get_sort_key() on everything. Polymorphism for the win! ๐
When Polymorphism Goes Wrong: The LEGO Mixing Disaster ๐จ
Not everything should be polymorphic. Sometimes you NEED to know the exact type:
class LegoSet:
def build(self):
return "Building awesome LEGO creation! ๐๏ธ"
class PuzzlePiece:
def build(self):
return "Putting puzzle together... ๐งฉ"
class PlayDoh:
def build(self):
return "Squishing Play-Doh into... something? ๐จ"
def construct_project(item):
"""
Polymorphic function - but maybe too polymorphic?
Everything has build() but they're VERY different!
"""
return item.build()
# This works, but is it RIGHT?
construct_project(LegoSet()) # Building awesome LEGO creation! ๐๏ธ
construct_project(PuzzlePiece()) # Putting puzzle together... ๐งฉ
construct_project(PlayDoh()) # Squishing Play-Doh into... something? ๐จ
# Sometimes you need to check types!
def construct_lego_only(item):
if not isinstance(item, LegoSet):
raise TypeError("Sorry, LEGO only! No Play-Doh allowed! ๐ซ")
return item.build()
construct_lego_only(LegoSet()) # โ
Works!
# construct_lego_only(PlayDoh()) # โ TypeError!
Lesson: Polymorphism is powerful, but donโt force it where it doesnโt make sense. Sometimes type checking is the right call! ๐ฏ
The LEGO Philosophy of Polymorphism ๐ง
Just like LEGOโs design:
- Universal Interface: All bricks connect (studs + tubes = magic)
- Specialized Behavior: Each brick type has unique features
- Mix & Match: Combine any bricks without knowing their exact types
- Extend Freely: Add new brick types without breaking existing builds
- Keep It Simple: If it clicks, it works! ๐
Your Mission, Should You Choose to Accept It ๐ฏ
Build a polymorphic LEGO theme park! Create:
-
Attractionbase class withoperate()anddescribe()methods -
RollerCoaster- makes people scream -
FerrisWheel- spins slowly with great views -
WaterSlide- splashes everyone -
HauntedHouse- spooky scary! - Create a
ThemeParkclass that can operate ALL attractions polymorphically - Add operator overloading so you can
+attractions together to create bigger parks!
Bonus: Make each attraction return a different emoji when operated! ๐ข๐ก๐ฆ๐ป
Next Time on โLike LEGO, Love Pythonโ ๐ฌ
In Episode 5, weโll explore Abstract Base Classes and Protocols - or as I like to call it, โHow LEGO Headquarters Enforces the Rules!โ Weโll learn about contracts, interfaces, and making sure every brick follows the master plan.
Until then, keep your interfaces clean, your duck typing quacking, and your operators overloaded with joy!
Happy building! ๐๏ธโจ
P.S. - Polymorphism is like the Universal LEGO Stud System. It doesnโt matter if youโre building with 1958 bricks or 2026 bricks - they all click together! Thatโs 68 years of backward compatibility. THATโS the power of a good interface! ๐งฑ๐
๐ฏ Key Takeaways:
- Polymorphism = same method name, different behaviors for different types
- Duck typing = if it has the method, it works (no type checking needed!)
- Method overriding = child classes customize parent methods
-
Operator overloading = make your classes work with
+,-,*, etc. - Use polymorphism to write flexible, extensible code
- Donโt force polymorphism where it doesnโt belong
- Real-world use: multi-cloud deployments, plugin systems, game entities
- LEGO studs > everything ๐
Top comments (0)