Skip to main content

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:

Legacy Codebase Issue:

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)​

# 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

The Solution: Python Descriptor Pattern​

Why Descriptors?​

Python Descriptors allow you to customize attribute access (__get__, __set__, __delete__) at the class level.

Key Advantages:

  1. Transparent - Existing code using connection.ip_address works unchanged
  2. Dynamic - Selection logic runs at access time, not definition time
  3. Policy-Driven - Can query external configuration for each access
  4. Centralized - All logic in one reusable descriptor class

Architecture & Workflow Diagram​

Key Points:

  1. Descriptor intercepts - __get__ called automatically on attribute access
  2. Policy-driven - Selects IPv4/IPv6 based on configuration
  3. Transparent - Existing code unchanged, zero modifications needed

Implementation​

Core Descriptor Class​

connection_ip_descriptor.py
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​

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

Patching the Device Class​

Dynamic Class Patching:

We patch the Connection class at runtime to install our descriptor, making it available to all connection instances.

Patching Logic
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​

config.json
{
"deployment_mode": "v4"
}

Behavior:

  • connection.ip_address β†’ Always returns IPv4
  • Error if IPv4 not available
  • IPv6 addresses ignored

Location-Level Configuration​

policy_config.json
{
"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 ModeLocation PolicyAllowedPreferFallbackBehavior
v4any['v4']-NoIPv4 only, error if missing
v6any['v6']-NoIPv6 only, error if missing
dualdefault=v4['v4']-NoIPv4 only (no fallback)
dualdefault=v6['v6']-NoIPv6 only (no fallback)
dualloc_A=v6['v4','v6']v6YesPrefer IPv6, fallback to IPv4
dualloc_B=v4['v4','v6']v4YesPrefer IPv4, fallback to IPv6

Usage Examples​

Transparent Access (No Code Changes)​

existing_application.py
# 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​

advanced_usage.py
# 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​

connection_initialization.py
# 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:

  1. Phase 1: Dual-mode deployment, all locations use IPv4 (default)
  2. Phase 2: Enable IPv6 on Location A (pilot)
  3. Phase 3: All locations to IPv6
Configuration
{
"deployment_mode": "dual",
"location_policies": {
"default": "v4"
}
}
Behavior
# 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!

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​

Python Data Model:

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​

descriptor_protocol.py
# 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​

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

Testing Strategy​

Unit Tests​

test_descriptor.py
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​

test_integration.py
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 (@property is enough)
  • Performance-critical tight loops
  • When explicit is better than implicit

References and Resources​

Documentation​

  • Proxy Pattern - Similar abstraction but different implementation
  • Strategy Pattern - Policy-based behavior selection
  • Decorator Pattern - Transparent behavior extension

Tools Used​

  • Python 3.8+ - For type hints and modern syntax
  • pytest - Testing framework