Duck Typing, Abstract Base Classes, and Context Managers in Python

Duck Typing and Polymorphism in Python

The principle of duck typing states: "If it walks like a duck and quacks like a duck, then it is a duck." In Python, this means that an object's behavior determines its classification rather than its inheritance hierarchy. If multiple classes implement the same method signature, they can be treated uniformly.

For instance:

  • An object with __iter__() or __getitem__() is considered iterable.
  • An object implementing both __iter__() and __next__() is recognized as an iterator.
  • Objects defining __enter__() and __exit__() are context managers.

This flexibility allows developers to add such behaviors by simply implementing the required special methods—no explicit inheritance or interface declaration needed.

class Cat:
    def speak(self):
        print("Meow")

class Dog:
    def speak(self):
        print("Woof")

class Duck:
    def speak(self):
        print("Quack")

animals = [Cat(), Dog(), Duck()]
for animal in animals:
    animal.speak()

Using Duck Typing with Built-in Functions

The built-in extend() method expects an iterable. By adding __getitem__() to a custom class, we make it compatible with extend(), even without formally inheriting from any sequence type.

class Team:
    def __init__(self, members):
        self.members = members

    def __getitem__(self, index):
        return self.members[index]

team = Team(["Alice", "Bob", "Charlie"])
group = ["David"]
group.extend(team)
print(group)  # Output: ['David', 'Alice', 'Bob', 'Charlie']

Abstract Base Classes (ABC)

Python’s abc module enables enforcing method implementation through abstract base classes. This is useful for creating interfaces that derived classes must follow.

For example, using Sized from collections.abc checks whether a class implements __len__():

from collections.abc import Sized

class Department:
    def __init__(self, staff):
        self.staff = staff

    def __len__(self):
        return len(self.staff)

dept = Department(["John", "Jane"])
print(isinstance(dept, Sized))  # True

To enforce method definitions, use @abstractmethod:

import abc

class Storage(abc.ABC):
    @abc.abstractmethod
    def save(self, key, data):
        pass

    @abc.abstractmethod
    def load(self, key):
        pass

class MemoryStorage(Storage):
    def __init__(self):
        self._data = {}

    def save(self, key, data):
        self._data[key] = data

    def load(self, key):
        return self._data.get(key)

# The following would raise TypeError at instantiation:
# class BadStorage(Storage): pass
# bad = BadStorage()  # Error: not all abstract methods implemented

Difference Between isinstance() and type()

isinstance() respects inheritance; type() does not.

class Parent:
    pass

class Child(Parent):
    pass

c = Child()

print(isinstance(c, Child))   # True
print(isinstance(c, Parent))  # True
print(type(c) is Child)       # True
print(type(c) is Parent)      # False

Class Variables vs Instance Variables

Class variables are shared across instances, while instance variables belong to individual objects.

class Item:
    category = "electronics"

    def __init__(self, name, price):
        self.name = name
        self.price = price

i1 = Item("Laptop", 999)
i2 = Item("Phone", 699)

print(i1.category)  # electronics
print(i2.category)  # electronics

Item.category = "gadgets"
print(i1.category)  # gadgets

i1.category = "legacy"  # Creates an instance variable
print(i1.category)      # legacy
print(i2.category)      # gadgets

Attribute lookup follows this order: instance namespace → class namespace → parent classes (via MRO).

Method Resolution Order (MRO) and Multiple Inheritance

Python uses the C3 linearization algorithm to determine method lookup order in multiple inheritance scenarios.

class X:
    pass

class Y(X):
    pass

class Z(X):
    pass

class W(Y, Z):
    pass

print(W.__mro__)
# (<class '__main__.W'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.X'>, <class 'object'>)

Instance Methods, Class Methods, and Static Methods

  • Instance methods: Take self, operate on instance data.
  • Class methods: Decorated with @classmethod, take cls, used for alternative constructors.
  • Static methods: Decorated with @staticmethod, no automatic reference passed; grouped logically within the class.
class Clock:
    def __init__(self, hour, minute, second):
        self.hour = hour
        self.minute = minute
        self.second = second

    def tick(self):
        self.second += 1
        if self.second == 60:
            self.second = 0
            self.minute += 1

    @classmethod
    def from_string(cls, time_str):
        h, m, s = map(int, time_str.split(':'))
        return cls(h, m, s)

    @staticmethod
    def is_valid_time(time_str):
        try:
            h, m, s = map(int, time_str.split(':'))
            return 0 <= h < 24 and 0 <= m < 60 and 0 <= s < 60
        except ValueError:
            return False

    def __str__(self):
        return f"{self.hour:02}:{self.minute:02}:{self.second:02}"

clock = Clock.from_string("14:35:20")
clock.tick()
print(clock)  # 14:35:21
print(Clock.is_valid_time("25:00:00"))  # False

Data Hiding and Name Mangling

Python doesn't have true private attributes, but naming an attribute with double underscores (__attr) triggers name mangling: it becomes _ClassName__attr.

class Person:
    def __init__(self, birth_year):
        self.__birth_year = birth_year

    def get_age(self, current_year=2023):
        return current_year - self.__birth_year

p = Person(1990)
print(p.get_age())           # 133
print(p._Person__birth_year) # 1990 (accessible via mangled name)

Introspection in Python

Python supports introspection—examining object structure at runtime.

  • obj.__dict__: Returns a dictionary of writable atributes.
  • dir(obj): Lists all attribute and methods (names only).
class Employee:
    title = "Engineer"

    def __init__(self, name):
        self.name = name

e = Employee("Sam")
print(e.__dict__)     # {'name': 'Sam'}
e.__dict__['location'] = 'Berlin'
print(e.location)     # Berlin
print(dir(e)[:5])     # Some attribute names including 'location', 'name', etc.

Understanding super() and Method Resolution

super() follows the MRO chain, not just immediate parent. It ensures cooperative multiple inheritance works correctly.

class A:
    def __init__(self):
        print("A init")

class B(A):
    def __init__(self):
        print("B init")
        super().__init__()

class C(A):
    def __init__(self):
        print("C init")
        super().__init__()

class D(B, C):
    def __init__(self):
        print("D init")
        super().__init__()

d = D()
# Output:
# D init
# B init
# C init
# A init
print(D.__mro__)

Mixin Classes in Practice

Mixins provide reusable functionality without being standalone classes. Common in frameworks like Django REST Framework:

class ListViewMixin:
    def list(self, request):
        queryset = self.get_queryset()
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

class RetrieveViewMixin:
    def retrieve(self, request, pk):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

class ProductViewSet(ListViewMixin, RetrieveViewMixin):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

Key traits of mixins:

  • Single responsibility.
  • No dependency on specific base class.
  • Avoid using super() unless necessary.
  • Suffix name with Mixin for clarity.

Context Managers and __enter__/__exit__

Context managers manage resource setup and teardown using with blocks.

class Resource:
    def __enter__(self):
        print("Acquiring resource")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(f"Handling exception: {exc_val}")
        print("Releasing resource")
        return False  # Propagate exceptions

    def process(self):
        print("Processing...")

with Resource() as res:
    res.process()

Simplifiyng Context Managers with contextlib

The @contextmanager decorator turns a generator into a context manager.

from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"Setting up {name}")
    resource = {}
    try:
        yield resource
    finally:
        print(f"Tearing down {name}")

with managed_resource("database") as db:
    db["user"] = "admin"
    print("Working with resource")

Tags: python duck-typing abstract-base-classes Mixins context-managers

Posted on Sat, 19 Sep 2026 16:19:03 +0000 by Deivas