Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
121: Polymorphism in Practice
You've probably heard the academic definition of polymorphism—something about "many forms"—but in the trenches of a real codebase, it's less about theory and more about how you handle growth. When I see junior devs struggling with a codebase that's becoming a "spaghetti monster," it's almost always because they're fighting against polymorphism instead of using it.
The "Type-Checking" Trap
Let's say we're building a payment processing system. You've got Credit Cards, PayPal accounts, and maybe some crypto wallets. The naive way to handle this is to create a central "manager" function that checks what it's dealing with before deciding what to do. It usually looks something like this:
def process_payment(payment_method, amount):
if isinstance(payment_method, CreditCard):
payment_method.charge_card(amount)
elif isinstance(payment_method, PayPal):
payment_method.send_paypal_payment(amount)
elif isinstance(payment_method, BitcoinWallet):
payment_method.transfer_crypto(amount)
else:
raise ValueError("Unsupported payment method")
At first glance, this feels safe. You know exactly which method is being called for which class. But here is where the technical debt starts accruing. Every time the business decides to add a new payment method—say, Apple Pay or Klarna—you have to go back into this specific function and add another elif block. You're modifying existing, working code to add new functionality, which is a great way to accidentally break your checkout flow on a Friday afternoon.
Letting the Objects Do the Work
The better way—the polymorphic way—is to stop asking the object what it is and start telling it what to do. We agree on a common interface. I don't care if it's a credit card or a magic bean; if it's a "Payment Method," it must have a method called pay().
class CreditCard:
def pay(self, amount):
print(f"Charging ${amount} to Credit Card")
class PayPal:
def pay(self, amount):
print(f"Redirecting to PayPal for ${amount}")
class BitcoinWallet:
def pay(self, amount):
print(f"Transferring {amount} BTC from wallet")
def process_payment(payment_method, amount):
# This function never needs to change again
payment_method.pay(amount)
Now, the process_payment function is blissfully ignorant. It doesn't know the classes exist; it just knows that whatever object it receives has a pay method. This is "Duck Typing" in its purest form: if it walks like a duck and quacks like a duck, Python treats it as a duck.
The Trade-off: Implicit vs. Explicit
You might be thinking, "But now I don't have that ValueError for unsupported types!" That's a fair point. By moving to polymorphism, you've traded a compile-time-ish check (the if/else) for a potential runtime AttributeError if you pass in an object that doesn't have a pay method.
In a small script, that's fine. In a massive enterprise system, I'd suggest using an Abstract Base Class (ABC) to enforce the contract. By inheriting from an ABC, Python will actually prevent you from even instantiating a payment class if you forgot to implement the pay method. It gives you the architectural cleanliness of polymorphism with the safety net of an explicit requirement. But the core philosophy remains: stop checking types and start trusting your interfaces.
📋 Practical Task
Refactoring the Multi-Channel Notification Dispatcher
You've inherited a piece of code that handles sending notifications to users. Currently, it's written using the "type-checking trap" we discussed. Your goal is to refactor this into a polymorphic system so that adding new notification channels (like Slack or Discord) doesn't require changing the dispatcher logic.
The Current Mess:
class EmailService:
def send_email(self, message):
print(f"Sending Email: {message}")
class SMSService:
def send_sms(self, message):
print(f"Sending SMS: {message}")
def notify_user(service, message):
if isinstance(service, EmailService):
service.send_email(message)
elif isinstance(service, SMSService):
service.send_sms(message)
Your Task:
- Modify
EmailServiceandSMSServiceso they share a common method name for sending messages. - Rewrite the
notify_userfunction so it no longer usesisinstanceorif/elifblocks. - Create a new class called
PushNotificationServicewith its own send method and verify thatnotify_usercan handle it without any modifications to the function's internal code.
There are no comments for now.