Core Python Programming Concepts: Magic Methods, Inheritance, Type Hints, and Closures
Dunder and Special Methods
Python classes suppport special methods prefixed and suffixed with double underscores to intercept built-in operations. These are often referred to as "dunder" methods.
String Representation
Implement __str__ to define how an instance renders when passed to print() or str(). Without it, Python falls back to the default object memory address.
class Device:
def __init__(self, model: str, firmware: int):
self.model = model
self.firmware = firmware
def __str__(self) -> str:
return f"Model:{self.model} | Build:{self.firmware}"
d = Device("X1", 3)
print(d) # Model:X1 | Build:3
print(str(d)) # Model:X1 | Build:3
Comparison Operations
Relational operators trigger specific dunder methods. __lt__ handles <, while __le__ manages <=.
class Component:
def __init__(self, id_: int, capacity: float):
self.id_ = id_
self.capacity = capacity
def __lt__(self, other) -> bool:
return self.capacity < other.capacity
def __le__(self, other) -> bool:
return self.capacity <= other.capacity
c1 = Component(1, 50.0)
c2 = Component(2, 75.0)
print(c1 < c2) # True
print(c1 <= c2) # True
Equality comparisons rely on __eq__, following the same pattern.
Encapsulation & Name Mangling
Attributes and methods prefixed with double underscores undergo name mangling. This restricts direct external access, enforcing internal visibility while allowing class internals to interact freely.
class NetworkAdapter:
__status_code = 200
def _private_check(self):
return "Active"
def run_diagnostic(self):
if self.__status_code == 200:
return "Online"
return self._private_check()
adapter = NetworkAdapter()
# Direct access triggers AttributeError
# print(adapter.__status_code)
print(adapter.run_diagnostic()) # Online
Inheritance Mechanics
A subclass can extend a base class (single inheritance) or derive from multiple bases (multiple inheritance). When parent classes share method or attribute names, Python applies the Method Resolution Order (MRO), prioritizing left-to-right inheritance.
class BaseUnit:
manufacturer = "Alpha"
def transmit(self):
print("Base signal sent")
class AdvancedUnit(BaseUnit):
firmware = "v4.0"
def transmit(self):
print("Advanced burst mode")
unit = AdvancedUnit()
print(unit.manufacturer) # Alpha
unit.transmit() # Advanced burst mode
Overriding & Parent Invocation
When a subclass overrides a parent's attribute or method, the new version takes precedence. To revert to or combine with the parent logic, reference the base class directly or use super().
class EnhancedUnit(AdvancedUnit):
def transmit(self):
print(f"Origin: {AdvancedUnit.manufacturer}")
super().transmit()
print("Custom enhancement applied")
e = EnhancedUnit()
e.transmit()
# Origin: Alpha
# Advanced burst mode
# Custom enhancement applied
Polymorphism & Abstract Patterns
Polymorphism allows uniform function calls across heterogeneous objects sharing a common interface. This is typically achieved through inheritance or abstract base definitions.
class Sensor:
def read_data(self):
raise NotImplementedError
class TempSensor(Sensor):
def read_data(self):
return "24°C"
class PressureSensor(Sensor):
def read_data(self):
return "1013 hPa"
def collect(s: Sensor):
print(s.read_data())
collect(TempSensor())
collect(PressureSensor())
Static Type Annotations
Type hints improve code readability and enable static analysis without affecting runtime behavior. They apply to variables, function parameters, and return values.
import random
counter: int = 42
labels: list[str] = ["A", "B"]
config: dict[str, float] = {"rate": 0.85}
combo_tuple: tuple[int, str, bool] = (1, "yes", True)
def parse_input(data: list[str]) -> int:
return len(data)
# Comment-style fallback for dynamic expressions
value = random.choice([1, 2, 3]) # type: int
Alternative types specify value boundaries using Union:
from typing import Union
payload: Union[str, int]
def resolve(val: Union[str, int]) -> str:
return str(val)
Closures & Nonlocal Scope
A closure captures variables from its enclosing lexical scope. Even after the outer funcsion completes execution, the nested function retains access to those captured values. Modifying the outer scope variable requires the nonlocal declaration.
def create_counter(start):
count = start
def increment(step):
nonlocal count
count += step
return count
return increment
tick = create_counter(5)
print(tick(2)) # 7
print(tick(3)) # 10
Function Decoration Patterns
Decorators wrap functions to inject pre- and post-execution logic. They internally rely on closures and must preserve the original signature using *args and **kwargs.
def log_execution(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@log_execution
def calculate(a, b):
return a + b
print(calculate(10, 5))
The @decorator_name syntax provides a cleaner alternative to manual assignment, automatically replacing the decorated function with the wrapped version.