Python Advanced Programming
This article covers the core features of Python’s object-oriented programming (OOP): encapsulation, inheritance, and polymorphism, along with advanced modern Python topics such as type annotations and abstract classes.
Encapsulation
Encapsulation binds data and the methods that operate on it together, and uses access control to restrict direct external access.
Private Attributes (Double-Underscore Mangling)
class BankAccount:
def __init__(self, owner: str, balance: float):
self.owner = owner # public attribute
self.__balance = balance # private attribute (mangled to _BankAccount__balance)
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
def get_balance(self) -> float:
return self.__balance
acc = BankAccount("Alice", 1000.0)
acc.deposit(500)
print(acc.get_balance()) # 1500.0
# print(acc.__balance) # AttributeError (not directly accessible externally)
print(acc._BankAccount__balance) # 1500.0 (accessible if you know the mangled name, but not recommended)The @property Decorator
@property disguises a method as an attribute access, while allowing you to add getter / setter / deleter logic:
class Circle:
def __init__(self, radius: float):
self.__radius = radius
@property
def radius(self) -> float:
return self.__radius
@radius.setter
def radius(self, value: float) -> None:
if value < 0:
raise ValueError("Radius cannot be negative")
self.__radius = value
@property
def area(self) -> float:
import math
return math.pi * self.__radius ** 2
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.539...
c.radius = 10 # triggers the setter
c.radius = -1 # ValueErrorInheritance
Inheritance lets a subclass reuse the attributes and methods of its parent class, and extend or override parent behavior.
Single Inheritance
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
raise NotImplementedError("Subclasses must implement speak")
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r})"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} says: Meow!"
dog = Dog("Rex")
print(dog.speak()) # Rex says: Woof!
print(dog) # Dog(name='Rex')Calling the Parent with super()
class Vehicle:
def __init__(self, brand: str, speed: int):
self.brand = brand
self.speed = speed
def info(self) -> str:
return f"{self.brand}, top speed {self.speed} km/h"
class ElectricCar(Vehicle):
def __init__(self, brand: str, speed: int, battery: int):
super().__init__(brand, speed) # call the parent __init__
self.battery = battery # new attribute
def info(self) -> str:
base = super().info()
return f"{base}, battery capacity {self.battery} kWh"
tesla = ElectricCar("Tesla", 250, 100)
print(tesla.info())
# Tesla, top speed 250 km/h, battery capacity 100 kWhMultiple Inheritance and MRO
Python uses the C3 linearization algorithm to compute the Method Resolution Order (MRO). You can inspect it with ClassName.mro():
class A:
def method(self): return "A"
class B(A):
def method(self): return "B"
class C(A):
def method(self): return "C"
class D(B, C):
pass
print(D.mro()) # [D, B, C, A, object]
print(D().method()) # "B"The Mixin Pattern
When multiple inheritance is needed, it is recommended to use Mixin classes to mix in behavior rather than mixing in “is-a” relationships:
class JSONMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class LogMixin:
def log(self, message: str) -> None:
print(f"[{self.__class__.__name__}] {message}")
class User(JSONMixin, LogMixin):
def __init__(self, name: str, age: int):
self.name = name
self.age = age
u = User("Alice", 25)
print(u.to_json()) # {"name": "Alice", "age": 25}
u.log("User created")Polymorphism
Polymorphism means different types of objects can use the same interface (method name) without caring about the concrete type:
animals: list[Animal] = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for animal in animals:
print(animal.speak()) # each object calls its own speak implementationDuck Typing
Python’s polymorphism does not require inheritance — as long as an object implements the required method, it can be used:
class Duck:
def quack(self): print("Quack!")
class Person:
def quack(self): print("I'm quacking like a duck!")
def make_it_quack(obj) -> None:
obj.quack() # no type check — just needs a quack method
make_it_quack(Duck()) # Quack!
make_it_quack(Person()) # I'm quacking like a duck!Abstract Classes (ABC)
Use the abc module to enforce that subclasses implement specific interfaces:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Calculate the area"""
@abstractmethod
def perimeter(self) -> float:
"""Calculate the perimeter"""
def describe(self) -> str:
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
rect = Rectangle(4, 6)
print(rect.describe()) # Area: 24.00, Perimeter: 20.00
# Shape() # TypeError: Can't instantiate abstract classClass Methods and Static Methods
class DateParser:
fmt = "%Y-%m-%d"
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_str: str) -> "DateParser":
"""Factory method: create an instance from a string"""
from datetime import datetime
d = datetime.strptime(date_str, cls.fmt)
return cls(d.year, d.month, d.day)
@staticmethod
def is_valid_date(date_str: str) -> bool:
"""Utility method: does not depend on the class or instance"""
try:
from datetime import datetime
datetime.strptime(date_str, "%Y-%m-%d")
return True
except ValueError:
return False
d = DateParser.from_string("2024-06-01")
print(d.year, d.month, d.day) # 2024 6 1
print(DateParser.is_valid_date("2024-13-01")) # FalseReflection
Reflection allows you to dynamically read and modify an object’s attributes and methods using strings. It is commonly used in plugin systems or dynamic routing:
class Config:
debug = False
port = 8080
host = "localhost"
cfg = Config()
# check whether an attribute exists
print(hasattr(cfg, "port")) # True
# get an attribute value (getattr supports a default)
print(getattr(cfg, "port")) # 8080
print(getattr(cfg, "timeout", 30)) # 30 (returned when the attribute does not exist)
# set an attribute
setattr(cfg, "debug", True)
print(cfg.debug) # True
# delete an attribute
setattr(cfg, "temp", "value")
delattr(cfg, "temp")
# dynamically call a method
class Router:
def get(self): return "GET handler"
def post(self): return "POST handler"
router = Router()
method = "get"
if hasattr(router, method):
handler = getattr(router, method)
print(handler()) # GET handlerMetaclasses
A metaclass is a class that creates other classes. type is the default metaclass for all classes:
# dynamically create a class with type
Dog = type("Dog", (object,), {
"sound": "Woof",
"speak": lambda self: f"{self.sound}!"
})
d = Dog()
print(d.speak()) # Woof!
# custom metaclass (uncommon; understanding it is enough)
class SingletonMeta(type):
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, url: str):
self.url = url
db1 = Database("postgres://localhost/app")
db2 = Database("mysql://localhost/app")
print(db1 is db2) # True (singleton — db2 is ignored)Type Annotations (Python 3.5+)
Modern Python recommends adding type annotations to function parameters and return values. Combined with tools like mypy or pyright, this enables static type checking:
from typing import Optional, Union, Callable
from collections.abc import Sequence
# basic annotations
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# optional parameter (Python 3.10+ can use str | None)
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
# Python 3.10+ Union shorthand
def process(value: int | str | None) -> str:
return str(value) if value is not None else "empty"
# generics (Python 3.9+ supports built-in types)
def flatten(matrix: list[list[int]]) -> list[int]:
return [x for row in matrix for x in row]
# Protocol (structural subtyping — formal expression of duck typing)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw()Python 3.12 new syntax: you can use the type keyword to define type aliases and the [T] syntax to define generic classes — much more concise than TypeVar:
type Vector = list[float]
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()