Python Descriptor Pattern: Dynamic IPv4/IPv6 Selection
Real-World Case Study: IPv6 Migration with Zero Code Changes
Problem Statementβ
The Challengeβ
During IPv6 migration in a large-scale infrastructure project, we faced a critical challenge:
Thousands of application scripts and libraries access a connection.ip_address attribute expecting a single IP address. How do we support dual-stack (IPv4/IPv6) without modifying all existing code?
Requirements:
- Support IPv4-only, IPv6-only, and dual-stack deployment modes
- Per-deployment IPv6 policies with fine-grained control
- Zero changes to existing scripts accessing
connection.ip_address - Backward compatibility with IPv4-only systems
- Dynamic IP selection based on infrastructure configuration
Traditional Approaches (and Why They Failed)β
- β Find & Replace
- β Property Method
- β Property with Hardcoded Logic
# BAD: Manual code changes everywhere
# Old code:
connection.ip_address # Returns IPv4
# New code (requires changing 1000+ files):
connection.ipv4_address # Explicit IPv4
connection.ipv6_address # Explicit IPv6
connection.get_ip(family='v6') # API change
Problems:
- Manual changes to thousands of test scripts
- Risk of breaking existing tests
- Maintenance nightmare
- No dynamic policy enforcement
# BAD: Requires code changes to use method
class Connection:
def get_ip(self, prefer='v4'):
"""Get IP with preference"""
if prefer == 'v6' and self.ipv6:
return self.ipv6
return self.ipv4
# All existing code breaks:
connection.ip_address # AttributeError
connection.get_ip() # Requires code changes
Problems:
- Breaks attribute access syntax
- Every script needs updates
- Not transparent
# BAD: No flexibility for different policies
class Connection:
@property
def ip_address(self):
# Hardcoded: always prefer IPv4
return self.ipv4 or self.ipv6
# Issues:
# - Can't enforce strict IPv6 requirements
# - Can't adapt to different deployment modes
# - No per-deployment policies
Problems:
- No policy-based selection
- Can't handle complex requirements
- Inflexible
The Solution: Python Descriptor Patternβ
Why Descriptors?β
Python Descriptors allow you to customize attribute access (__get__, __set__, __delete__) at the class level.
Key Advantages:
- Transparent - Existing code using
connection.ip_addressworks unchanged - Dynamic - Selection logic runs at access time, not definition time
- Policy-Driven - Can query external configuration for each access
- Centralized - All logic in one reusable descriptor class
Architecture & Workflow Diagramβ
Key Points:
- Descriptor intercepts -
__get__called automatically on attribute access - Policy-driven - Selects IPv4/IPv6 based on configuration
- Transparent - Existing code unchanged, zero modifications needed
Implementationβ
Core Descriptor Classβ
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
class IPAddressDescriptor:
"""
Property descriptor that dynamically selects ip_address based on policy.
Intercepts attribute access and returns IPv4 or IPv6 based on:
- Deployment mode (v4, v6, dual)
- Location-specific policies
- System configuration
"""
def __get__(self, obj, objtype=None):
"""Called when accessing connection.ip_address"""
if obj is None:
return self
# Get stored addresses
v4 = getattr(obj, '_ipv4_address', None)
v6 = getattr(obj, '_ipv6_address', None)
# Get selection policy
policy = self._get_policy(obj)
# Select and return appropriate IP
return self._select_ip(obj, v4, v6, policy)
def __set__(self, obj, value):
"""Called when setting connection.ip_address = "x.x.x.x" """
import ipaddress
try:
ip_obj = ipaddress.ip_address(value)
if ip_obj.version == 4:
setattr(obj, '_ipv4_address', value)
else:
setattr(obj, '_ipv6_address', value)
except ValueError:
# Backward compatibility: assume IPv4
setattr(obj, '_ipv4_address', value)
def _get_policy(self, obj) -> Dict[str, Any]:
"""Get IP selection policy (implementation in next section)."""
pass
def _select_ip(self, obj, v4, v6, policy) -> Optional[str]:
"""Select IP based on policy (implementation in next section)."""
pass
Policy Resolution Logicβ
- Get Policy
- Select IP
def _get_policy(self, connection) -> Dict[str, Any]:
"""
Compute IP selection policy based on:
- Deployment mode: v4, v6, dual
- Location configuration
"""
# Get configuration
config = getattr(connection, 'config', None)
# Get connection location
location = getattr(connection, 'location', '')
# Get deployment mode
mode = getattr(config, 'deployment_mode', None)
# Compute policy using config engine
policy = config.compute_ip_selection_policy(location, mode)
# Returns: {
# 'allowed': ['v4'] or ['v6'] or ['v4', 'v6'],
# 'prefer': 'v4' or 'v6' or None,
# 'strict': True/False,
# 'fallback_ok': True/False
# }
return policy
def _select_ip(self, connection, v4, v6, policy) -> Optional[str]:
"""Select IP based on policy."""
allowed = policy.get('allowed', ['v4'])
prefer = policy.get('prefer')
fallback_ok = policy.get('fallback_ok', False)
connection_name = getattr(connection, 'name', 'unknown')
# IPv4 ONLY mode
if allowed == ['v4']:
if v4:
return v4
raise RuntimeError(
f"IPv4 required by policy but not available for {connection_name}"
)
# IPv6 ONLY mode
if allowed == ['v6']:
if v6:
return v6
raise RuntimeError(
f"IPv6 required by policy but not available for {connection_name}"
)
# DUAL mode with preference
if prefer == 'v6':
if v6:
return v6
if v4 and fallback_ok:
logger.info(f"Prefer IPv6, falling back to IPv4")
return v4
if v4: # No fallback allowed, but have IPv4
return v4
else: # prefer v4 or None (default to v4)
if v4:
return v4
if v6 and fallback_ok:
logger.info(f"Prefer IPv4, falling back to IPv6")
return v6
if v6: # No fallback allowed, but have IPv6
return v6
# No IP available at all
raise RuntimeError(
f"No valid IP available for {connection_name}. "
f"Policy: {policy}, IPv4: {v4}, IPv6: {v6}"
)
Patching the Device Classβ
We patch the Connection class at runtime to install our descriptor, making it available to all connection instances.
def patch_connection_class():
"""
Patch Connection class with dynamic ip_address property.
Called once during initialization.
"""
from your_module import Connection
# Save original if exists
if hasattr(Connection, 'ip_address'):
Connection._original_ip_address = Connection.ip_address
# Install descriptor
Connection.ip_address = IPAddressDescriptor()
# Add explicit access properties
Connection.ipv4_address = property(
lambda self: getattr(self, '_ipv4_address', None),
lambda self, val: setattr(self, '_ipv4_address', val)
)
Connection.ipv6_address = property(
lambda self: getattr(self, '_ipv6_address', None),
lambda self, val: setattr(self, '_ipv6_address', val)
)
logger.info("β Connection class patched with dynamic ip_address")
Policy Configuration Examplesβ
Deployment Mode Configurationβ
- IPv4 Only
- IPv6 Only
- Dual Mode
{
"deployment_mode": "v4"
}
Behavior:
connection.ip_addressβ Always returns IPv4- Error if IPv4 not available
- IPv6 addresses ignored
{
"deployment_mode": "v6"
}
Behavior:
connection.ip_addressβ Always returns IPv6- Error if IPv6 not available
- IPv4 addresses ignored
{
"deployment_mode": "dual"
}
Behavior:
connection.ip_addressβ Returns IPv4 or IPv6 based on location policy- Supports per-location configuration
- Enables gradual IPv6 migration
Location-Level Configurationβ
{
"location_policies": {
"default": "v4", // Default: IPv4 only, no fallback
"loc_A": "v6", // Location A: IPv6 only, no fallback
"loc_C": "v4" // Location C: IPv4 only
}
}
Policy Matrixβ
| Deployment Mode | Location Policy | Allowed | Prefer | Fallback | Behavior |
|---|---|---|---|---|---|
v4 | any | ['v4'] | - | No | IPv4 only, error if missing |
v6 | any | ['v6'] | - | No | IPv6 only, error if missing |
dual | default=v4 | ['v4'] | - | No | IPv4 only (no fallback) |
dual | default=v6 | ['v6'] | - | No | IPv6 only (no fallback) |
dual | loc_A=v6 | ['v4','v6'] | v6 | Yes | Prefer IPv6, fallback to IPv4 |
dual | loc_B=v4 | ['v4','v6'] | v4 | Yes | Prefer IPv4, fallback to IPv6 |
Usage Examplesβ
Transparent Access (No Code Changes)β
# Existing application code works unchanged!
from your_module import get_connection
# Get connection object
connection = get_connection('api_server')
# This line works with ZERO changes:
ip_address = connection.ip_address # β Returns IPv4 or IPv6 based on policy
# Use IP in API calls, connections, etc.
response = requests.get(f"https://{ip_address}/api/v1/data")
Explicit Access When Neededβ
# When you specifically need IPv4 or IPv6:
# Get IPv4 explicitly
ipv4 = connection.ipv4_address # '10.1.1.1'
# Get IPv6 explicitly
ipv6 = connection.ipv6_address # '2001:db8::1'
# Dynamic (policy-based)
ip = connection.ip_address # Selected based on policy
Setting IP Addressesβ
# Automatic IP family detection
connection.ip_address = "10.1.1.1" # β Stored as IPv4
connection.ip_address = "2001:db8::1" # β Stored as IPv6
# Or explicit:
connection.ipv4_address = "10.1.1.1"
connection.ipv6_address = "2001:db8::1"
Real-World Scenario Walkthroughβ
Scenario: Gradual IPv6 Migrationβ
Migration Plan:
- Phase 1: Dual-mode deployment, all locations use IPv4 (default)
- Phase 2: Enable IPv6 on Location A (pilot)
- Phase 3: All locations to IPv6
- Phase 1: IPv4 Default
- Phase 2: Location A Pilot
- Phase 3: All IPv6
{
"deployment_mode": "dual",
"location_policies": {
"default": "v4"
}
}
# All connections use IPv4
connection_locA.ip_address # β '10.1.1.1' (IPv4)
connection_locB.ip_address # β '10.1.2.1' (IPv4)
# No application code changes needed!
{
"deployment_mode": "dual",
"location_policies": {
"default": "v4",
"loc_A": "v6" // β Enable IPv6 for Location A
}
}
# Location A prefers IPv6, falls back to IPv4
connection_locA.ip_address # β '2001:db8::1' (IPv6) β
# Fallback if IPv6 not ready:
connection_locA.ip_address # β '10.1.1.1' (IPv4 fallback) β
# Other locations unchanged
connection_locB.ip_address # β '10.1.2.1' (IPv4)
# Still no code changes!
{
"deployment_mode": "v6", // β System-wide IPv6
"location_policies": {
"default": "v6"
}
}
# All connections use IPv6
connection_locA.ip_address # β '2001:db8::1'
connection_locB.ip_address # β '2001:db8:2::1'
connection_locC.ip_address # β '2001:db8:3::1'
# Migration complete - zero application changes!
Key Benefitsβ
Zero Code Changesβ
No modifications to existing applications or libraries. connection.ip_address just works.
Policy-Drivenβ
Centralized configuration controls IP selection across entire test suite.
Gradual Migrationβ
Per-site policies enable incremental IPv6 rollout with safety nets.
Backward Compatibleβ
Defaults to IPv4-only behavior when configuration is absent.
Python Descriptor Deep Diveβ
How Descriptors Workβ
Descriptors implement the descriptor protocol by defining __get__, __set__, and/or __delete__ methods. When you access an attribute on an instance, Python checks if the class has a descriptor for that attribute and calls its methods.
Descriptor Protocol in Actionβ
# When you access: connection.ip_address
# Python calls: IPAddressDescriptor.__get__(connection, Connection)
# And returns: '10.1.1.1' or '2001:db8::1'
# When you set: connection.ip_address = '192.168.1.1'
# Python calls: IPAddressDescriptor.__set__(connection, '192.168.1.1')
# And stores in: connection._ipv4_address
# When you access: Connection.ip_address (at class level)
# Python calls: IPAddressDescriptor.__get__(None, Connection)
# And returns: <IPAddressDescriptor object>
This automatic delegation enables our transparent policy-driven IP selection!
Descriptor vs Propertyβ
- @property (Simple)
- Descriptor (Advanced)
class Connection:
def __init__(self):
self._ip = "10.1.1.1"
@property
def ip_address(self):
"""Simple property - static logic"""
return self._ip
@ip_address.setter
def ip_address(self, value):
self._ip = value
# Limitation: Logic is instance-specific,
# can't share behavior across classes
Use when:
- Simple attribute access
- No shared logic needed
- No external configuration
class IPAddressDescriptor:
"""Descriptor - reusable across classes"""
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Complex logic with external config
policy = self._get_policy(obj)
return self._select_ip(obj, policy)
def __set__(self, obj, value):
# Automatic IP family detection
...
# Apply to class
Connection.ip_address = IPAddressDescriptor()
# All instances share descriptor logic
Use when:
- Complex access logic
- Policy-driven behavior
- Reusable across classes
- External configuration needed
Testing Strategyβ
Unit Testsβ
import pytest
from ip_descriptor import IPAddressDescriptor, patch_connection_class
def test_ipv4_only_mode():
"""Test IPv4-only deployment mode."""
# Setup
connection = create_mock_connection(
location='loc_A',
deployment_mode='v4',
ipv4='10.1.1.1',
ipv6='2001:db8::1'
)
# Test
result = connection.ip_address
# Assert
assert result == '10.1.1.1' # IPv4 selected
assert result != '2001:db8::1' # IPv6 ignored
def test_dual_mode_fallback():
"""Test dual-mode with fallback."""
connection = create_mock_connection(
location='loc_C',
policy_mode='v6', # Prefer v6
deployment_mode='dual',
ipv4='10.1.1.1',
ipv6=None # Missing, should fallback
)
result = connection.ip_address
assert result == '10.1.1.1' # Fell back to IPv4
Integration Testsβ
def test_real_connection_loading():
"""Test with actual connection objects."""
from your_module import get_connection
# Patch Connection class
patch_connection_class()
# Get connection
connection = get_connection('api_endpoint')
# Verify descriptor installed
assert isinstance(Connection.ip_address, IPAddressDescriptor)
# Verify dynamic selection
ip = connection.ip_address
assert ip in [connection.ipv4_address, connection.ipv6_address]
def test_backward_compatibility():
"""Ensure existing applications still work."""
# Old application code that uses connection.ip_address
connection.ip_address = "10.1.1.1"
# Should still work
assert connection.ip_address == "10.1.1.1"
# Should auto-detect as IPv4
assert connection.ipv4_address == "10.1.1.1"
assert connection.ipv6_address is None
Lessons Learnedβ
What Worked Wellβ
Backward Compatibility - Zero changes to existing codebase saved months of work.
Policy-Driven Design - Centralized configuration made migration manageable.
Gradual Rollout - Per-site policies enabled safe incremental migration.
Alternative Approaches Consideredβ
Metaclassβ
class DeviceMeta(type):
def __new__(mcs, name, bases, dct):
# Modify class at creation time
...
Rejected: Too invasive, harder to debug, metaclass conflicts.
Proxy Patternβ
class DeviceProxy:
def __init__(self, connection):
self._connection = connection
@property
def ip_address(self):
# Proxy access
...
Rejected: Requires wrapping all devices, breaks type checking.
Monkey Patching Propertyβ
Connection.ip_address = property(lambda self: select_ip(self))
Rejected: Can't store state (descriptor instance), less clean.
Conclusionβ
Key Takeawaysβ
Python descriptors are a powerful tool for:
- Transparent attribute access customization
- Policy-driven behavior without code changes
- Clean separation of concerns
- Backward compatibility during migrations
When to use:
- Need to intercept attribute access
- Complex selection logic based on external config
- Zero-change migration requirements
- Shared behavior across many classes
When NOT to use:
- Simple attribute access (
@propertyis enough) - Performance-critical tight loops
- When explicit is better than implicit