Back to blog

Duck Typing vs Explicit Interface vs Implicit Interface: A Comprehensive Comparison

Programming ParadigmsInterfacesType SystemsPythonTypeScriptJavaC#Go

Introduction

In the world of programming, how we define and enforce contracts between different parts of our code varies significantly across languages. This article explores three distinct approaches: duck typing (Python, TypeScript), explicit interface implementation (Java, C#), and implicit interface implementation (Go). Understanding these paradigms helps you choose the right language for your project and write more maintainable code.

What Are Interfaces?

An interface defines a contract—a set of methods or properties that a type must implement. However, how this contract is defined and enforced differs dramatically across programming paradigms.

1. Duck Typing: "If It Walks Like a Duck..."

Concept

Duck typing follows the principle: "If it walks like a duck and quacks like a duck, then it must be a duck." The focus is on what an object can do rather than what it is. No formal interface declaration is required.

Python Example

class Duck:
    def quack(self):
        return "Quack!"
    
    def fly(self):
        return "Flying!"
 
class Person:
    def quack(self):
        return "I'm imitating a duck!"
    
    def fly(self):
        return "I'm flapping my arms!"
 
class Airplane:
    def fly(self):
        return "Flying at 500 mph!"
 
def make_it_fly(thing):
    # No type checking - just call the method
    return thing.fly()
 
# All work without explicit interface declaration
duck = Duck()
person = Person()
airplane = Airplane()
 
print(make_it_fly(duck))      # Flying!
print(make_it_fly(person))    # I'm flapping my arms!
print(make_it_fly(airplane))  # Flying at 500 mph!

TypeScript Example (Structural Typing)

interface Flyable {
    fly(): string;
}
 
class Bird {
    fly(): string {
        return "Bird is flying!";
    }
}
 
class Rocket {
    fly(): string {
        return "Rocket is launching!";
    }
    
    // Additional method not in interface
    ignite(): void {
        console.log("Igniting engines...");
    }
}
 
function makeItFly(thing: Flyable): string {
    return thing.fly();
}
 
const bird = new Bird();
const rocket = new Rocket();
 
// Works because both have fly() method - structural compatibility
console.log(makeItFly(bird));   // Bird is flying!
console.log(makeItFly(rocket)); // Rocket is launching!

Advantages

  • Flexibility: Any object with the right methods works, regardless of class hierarchy
  • Less boilerplate: No need to declare interface relationships
  • Rapid prototyping: Easy to test and iterate quickly
  • Polymorphism without inheritance: Achieve polymorphic behavior naturally

Disadvantages

  • Runtime errors: Type mismatches discovered at runtime (Python) or require strict type checking (TypeScript)
  • Less explicit: Harder to understand what methods an object needs without documentation
  • Refactoring challenges: Renaming methods requires searching all usage sites
  • IDE limitations: Less autocomplete support (especially in Python)

2. Explicit Interface Implementation: The Contract is King

Concept

Classes must explicitly declare that they implement an interface. The compiler enforces that all interface methods are implemented. This is the traditional object-oriented approach.

Java Example

interface Flyable {
    String fly();
}
 
interface Swimmable {
    String swim();
}
 
class Duck implements Flyable, Swimmable {
    @Override
    public String fly() {
        return "Duck is flying!";
    }
    
    @Override
    public String swim() {
        return "Duck is swimming!";
    }
}
 
class Airplane implements Flyable {
    @Override
    public String fly() {
        return "Airplane is flying at 500 mph!";
    }
}
 
// This won't compile - Boat doesn't implement Flyable
// class Boat implements Flyable {
//     // Missing fly() method - compilation error!
// }
 
class AnimalSimulator {
    public static void makeItFly(Flyable thing) {
        System.out.println(thing.fly());
    }
    
    public static void main(String[] args) {
        Duck duck = new Duck();
        Airplane airplane = new Airplane();
        
        makeItFly(duck);      // Duck is flying!
        makeItFly(airplane);  // Airplane is flying at 500 mph!
    }
}

C# Example

interface IFlyable
{
    string Fly();
}
 
interface ISwimmable
{
    string Swim();
}
 
class Duck : IFlyable, ISwimmable
{
    public string Fly()
    {
        return "Duck is flying!";
    }
    
    public string Swim()
    {
        return "Duck is swimming!";
    }
}
 
class Helicopter : IFlyable
{
    public string Fly()
    {
        return "Helicopter is hovering!";
    }
}
 
class Program
{
    static void MakeItFly(IFlyable thing)
    {
        Console.WriteLine(thing.Fly());
    }
    
    static void Main()
    {
        var duck = new Duck();
        var helicopter = new Helicopter();
        
        MakeItFly(duck);       // Duck is flying!
        MakeItFly(helicopter); // Helicopter is hovering!
    }
}

Advantages

  • Compile-time safety: Errors caught before runtime
  • Clear contracts: Explicit declaration of what a class can do
  • Better tooling: Excellent IDE support with autocomplete and refactoring
  • Self-documenting: Easy to see what interfaces a class implements
  • Enterprise-friendly: Large codebases benefit from explicit contracts

Disadvantages

  • Verbosity: More boilerplate code required
  • Rigid: Can't use existing classes without modification
  • Tight coupling: Classes explicitly tied to interface declarations
  • Wrapper hell: Need adapter/wrapper classes to make third-party code fit interfaces

3. Implicit Interface Implementation: Go's Middle Ground

Concept

Go provides interfaces, but types automatically satisfy an interface if they have the required methods. No explicit declaration needed, yet compile-time type safety is maintained.

Go Example

package main
 
import "fmt"
 
// Define interfaces
type Flyable interface {
    Fly() string
}
 
type Swimmable interface {
    Swim() string
}
 
// Duck implements both interfaces implicitly
type Duck struct{}
 
func (d Duck) Fly() string {
    return "Duck is flying!"
}
 
func (d Duck) Swim() string {
    return "Duck is swimming!"
}
 
// Airplane only implements Flyable
type Airplane struct{}
 
func (a Airplane) Fly() string {
    return "Airplane is flying at 500 mph!"
}
 
// Fish only implements Swimmable
type Fish struct{}
 
func (f Fish) Swim() string {
    return "Fish is swimming!"
}
 
// Functions accepting interfaces
func MakeItFly(f Flyable) {
    fmt.Println(f.Fly())
}
 
func MakeItSwim(s Swimmable) {
    fmt.Println(s.Swim())
}
 
func main() {
    duck := Duck{}
    airplane := Airplane{}
    fish := Fish{}
    
    // Duck can fly and swim
    MakeItFly(duck)    // Duck is flying!
    MakeItSwim(duck)   // Duck is swimming!
    
    // Airplane can only fly
    MakeItFly(airplane) // Airplane is flying at 500 mph!
    
    // Fish can only swim
    MakeItSwim(fish)    // Fish is swimming!
    
    // This would cause a compile-time error:
    // MakeItFly(fish) // fish doesn't have Fly() method
}

Interface Composition in Go

// Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
type Closer interface {
    Close() error
}
 
// Composed interface
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}
 
// Any type with Read(), Write(), and Close() methods
// automatically implements ReadWriteCloser
type File struct {
    // ... fields
}
 
func (f *File) Read(p []byte) (n int, err error) {
    // implementation
    return 0, nil
}
 
func (f *File) Write(p []byte) (n int, err error) {
    // implementation
    return 0, nil
}
 
func (f *File) Close() error {
    // implementation
    return nil
}
 
// File now implicitly implements ReadWriteCloser!

Advantages

  • Compile-time safety: Type checking at compile time like Java/C#
  • Flexibility: No need to modify existing code to satisfy interfaces
  • Decoupling: Types don't need to know about interfaces
  • Interface segregation: Easy to define small, focused interfaces
  • Third-party friendly: Can define interfaces for external libraries

Disadvantages

  • Less discoverable: Harder to find what interfaces a type implements
  • Accidental implementation: May accidentally satisfy an interface
  • Tooling dependency: Need good IDE to discover interface satisfaction
  • No explicit intent: Can't tell if implementation was intentional

Comparison Table

AspectDuck Typing (Python)Duck Typing (TypeScript)Explicit (Java/C#)Implicit (Go)
Type SafetyRuntimeCompile-time (with strict mode)Compile-timeCompile-time
DeclarationNoneOptional interfaceRequiredNone
CouplingVery looseLooseTightLoose
BoilerplateMinimalMinimalHighModerate
Tooling SupportLimitedExcellentExcellentGood
RefactoringDifficultGoodExcellentGood
Third-party CodeEasyEasyDifficultEasy
DiscoverabilityPoorGoodExcellentModerate
Learning CurveEasyModerateModerateModerate

Real-World Use Cases

Use Duck Typing When:

  • Building prototypes or MVPs
  • Writing scripts or small utilities
  • Maximum flexibility is needed
  • Working with dynamic data structures
  • Team prefers rapid iteration over type safety

Use Explicit Interfaces When:

  • Building large enterprise applications
  • Multiple teams working on same codebase
  • Long-term maintainability is critical
  • Domain models are well-defined upfront
  • Strong tooling support is required

Use Implicit Interfaces When:

  • Need both flexibility and type safety
  • Working with third-party libraries
  • Want to avoid modification of existing code
  • Prefer composition over inheritance
  • Building microservices or distributed systems

Best Practices

For Duck Typing (Python)

from typing import Protocol
 
# Use Protocol for static type checking
class Flyable(Protocol):
    def fly(self) -> str: ...
 
def make_it_fly(thing: Flyable) -> str:
    return thing.fly()
 
# Add docstrings to document expected behavior
def process_data(handler):
    """
    Args:
        handler: Object with process() method that returns str
    """
    return handler.process()

For TypeScript

// Use interfaces for better documentation
interface Database {
    connect(): Promise<void>;
    query(sql: string): Promise<any>;
    disconnect(): Promise<void>;
}
 
// Enable strict mode in tsconfig.json
// "strict": true

For Java/C#

// Follow Interface Segregation Principle
interface Readable {
    String read();
}
 
interface Writable {
    void write(String data);
}
 
// Don't create fat interfaces
// interface BadDataHandler {
//     void read();
//     void write();
//     void delete();
//     void update();
//     void validate();
//     void transform();
// }

For Go

// Keep interfaces small (1-3 methods)
type Stringer interface {
    String() string
}
 
// Define interfaces in consumer package, not provider
// (Accept interfaces, return structs)
 
// Bad: returning interface
func NewUser() UserInterface {
    return &User{}
}
 
// Good: returning concrete type
func NewUser() *User {
    return &User{}
}

Conclusion

Each approach to interfaces has its place in software development:

  • Duck typing excels in rapid prototyping and dynamic scenarios where flexibility trumps type safety
  • Explicit interfaces shine in large, long-lived codebases where clear contracts and compile-time safety are paramount
  • Implicit interfaces offer a sweet spot: compile-time safety with the flexibility to work with existing code

The choice depends on your project's needs:

  • Project size: Larger projects benefit from explicit contracts
  • Team size: Bigger teams need clearer contracts
  • Change frequency: More changes favor flexibility
  • Performance requirements: Static typing enables better optimization
  • Domain complexity: Complex domains benefit from explicit modeling

Understanding these trade-offs helps you choose the right tool for the job and write code that your team can maintain for years to come.

Further Reading

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.