Polymorphism: The Code That Bends Without Breaking (C# vs. Python)

Mid/Senior Engineer Asked at: Microsoft, Google, Amazon, Meta

Q: Can you explain polymorphism and compare how it's implemented in a statically-typed language like C# versus a dynamically-typed language like Python?

Why this matters: This isn't a vocabulary test. It’s a probe into your architectural philosophy. Your answer shows if you can build systems that embrace change or systems that shatter under its pressure. It reveals your understanding of the fundamental trade-offs between safety and flexibility.

Interview frequency: High. A classic question that bridges language specifics with high-level design principles.

❌ The Death Trap

Candidates fall flat by reciting dry, academic definitions and listing language features without connecting them to a purpose.

"Polymorphism means 'many forms'. In C#, you use interfaces or inherit from a base class and override virtual methods. In Python, it's called duck typing; you just call the method on the object."

You've stated facts, but you haven't demonstrated wisdom. The interviewer learns what you've memorized, not how you think.

🔄 The Reframe

What they're really asking: "How do you write a piece of code that can operate on objects that haven't even been invented yet? How do you decouple the 'what' from the 'how' to build an anti-fragile system?"

This reframes polymorphism as a strategic tool for managing future uncertainty. Your answer should be a story about future-proofing your code.

🧠 The Mental Model

The "Universal Power Outlet" model. Your core application is the building's electrical grid. It shouldn't care what you plug in.

1. The System: The Power Grid. This is your stable, high-level business logic (e.g., a notification service). Its job is simple: deliver power.
2. The Contract: The Wall Outlet. This is the standardized interface. It defines *how* new things connect to your system.
3. The Implementations: The Appliances. A phone charger, a lamp, a laptop. Each does something different, but they all conform to the outlet's contract.
4. The Difference in Philosophy:
C# (Static Polymorphism): The outlet has a specific, rigid shape (an `interface` or `virtual` method signature). The compiler checks the plug at compile time. It's safe and explicit. You can't plug in the wrong thing.
Python (Dynamic Polymorphism): The outlet is simpler. The rule is "if it has prongs that fit, it might work" (Duck Typing). It's flexible and requires less ceremony, but the check happens when you plug it in at runtime.

📖 The War Story

Situation: "I was tasked with building a document export feature. The system needed to take internal data and export it as a file."

Challenge: "The first request was for PDF export. But the product manager explicitly said, 'We'll need CSV next quarter, and probably Word .DOCX and JSON after that.' A simple `ExportToPdf()` function would be a dead end."

Stakes: "If we hardcoded the PDF logic, every new format would require changing the core, stable export service. This would mean more complex code, higher risk of bugs, and slower delivery of new features."

✅ The Answer

My Thinking Process:

"The core logic—fetch data, pass it to an exporter, return the file—should never change. The part that *will* change is the specific file format generation. This is the exact problem polymorphism is designed to solve: isolating the stable parts of a system from the volatile parts."

The C# Approach: Explicit Contracts

In C#, we formalize the contract with an `interface`. This is our "wall outlet." It's a compile-time promise that any class implementing it will have a specific shape.

public interface IDocumentExporter { byte[] Export(ReportData data); string ContentType { get; } string FileExtension { get; } } // The stable service only knows about the interface. public class ExportService { public FileResult ExportDocument(IDocumentExporter exporter, ReportData data) { var fileBytes = exporter.Export(data); // Polymorphic call return new File(fileBytes, exporter.ContentType); } }

To add a new format, we just create a new class. The `ExportService` doesn't need to be touched. The compiler guarantees safety. This is polymorphism built on explicit, verifiable contracts.

The Python Approach: Implicit Contracts

In Python, we don't need a formal `interface`. We rely on a convention, a shared understanding. This is "duck typing": if it walks like an exporter and quacks like an exporter, we treat it as one.

class PdfExporter: def export(self, data): # ... logic to create a PDF ... return pdf_bytes content_type = "application/pdf" file_extension = ".pdf" # The stable service just expects an object with the right methods/attributes. class ExportService: def export_document(self, exporter, data): file_bytes = exporter.export(data) # Polymorphic call return File(file_bytes, exporter.content_type)

The code is more concise. We can pass *any* object to `export_document` as long as it has an `export` method and the required attributes. The check happens at runtime. This is polymorphism built on convention and trust.

The Outcome & Trade-offs:

"Both approaches allowed us to add CSV and JSON exporters in a few hours with zero changes to the `ExportService`. The core system remained stable and untouched.

The key difference is the trade-off. C# gave us compile-time safety and self-documenting code; the `interface` is a clear guide. Python gave us speed of development and flexibility; it was less code to write, but it relies on developers knowing the implicit contract and having good tests to catch runtime errors."

🎯 The Memorable Hook

This frames the technical difference in terms of philosophy and risk management, showing a deeper level of architectural thinking.

💭 Inevitable Follow-ups

Q (for C#): "Besides interfaces, what's another way to achieve runtime polymorphism in C#?"

Be ready: "Using a base class with `virtual` methods, which subclasses can then `override`. This is useful when you want to share some common implementation among all subclasses, whereas an interface is a pure contract with no implementation."

Q (for Python): "What are the risks of duck typing, and how can you make it safer?"

Be ready: "The main risk is runtime errors if an object doesn't meet the implicit contract. You mitigate this with comprehensive unit tests. For more formal contracts, you can use Abstract Base Classes (`abc` module) to blend Python's flexibility with some of the explicit contract-checking of static languages. Modern Python also encourages type hints, which allow static analysis tools to catch these errors before runtime."

Written by Benito J D