How to Expand a Contract and Migrate Data in Python

Expand an old data contract by renaming fields and adding defaults, then migrate to a final version with deepcopy isolation.

Medium Python 3.9+ Aug 9, 2026 Production deployment patterns 14 views 0 copies

Python code

50 lines
Python 3.9+
import json
from copy import deepcopy

# Mock data representing a user record (old contract)
old_contract = {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30,
    "status": "active"
}

# Expanded contract: adds fields with defaults and renames some fields
expand_rules = {
    "id": "user_id",
    "name": "full_name",
    "email": "email",
    "age": "age",
    "status": "account_status"
}

# Migration: from old to expanded contract (add fields, rename keys)
def expand_contract(data, rules):
    expanded = {}
    for old_key, new_key in rules.items():
        if old_key in data:
            expanded[new_key] = data[old_key]
    # Add new fields with default values
    expanded.setdefault("created_at", "2024-01-01")
    expanded.setdefault("last_login", None)
    expanded.setdefault("premium", False)
    return expanded

# Mock: simulate data after expansion
expanded_data = expand_contract(old_contract, expand_rules)

# Contract migration: from expanded to final contract (contract version 2)
def migrate_contract(data):
    mig = deepcopy(data)
    # Rename again or restructure
    mig["name"] = mig.pop("full_name")
    mig["id"] = mig.pop("user_id")
    return mig

# Usage demonstration
if __name__ == "__main__":
    print("Old contract:", json.dumps(old_contract))
    print("Expanded:", json.dumps(expanded_data))
    final = migrate_contract(expanded_data)
    print("Migrated:", json.dumps(final))

Output

stdout
Old contract: {"id": 1, "name": "Alice", "email": "alice@example.com", "age": 30, "status": "active"}
Expanded: {"user_id": 1, "full_name": "Alice", "email": "alice@example.com", "age": 30, "account_status": "active", "created_at": "2024-01-01", "last_login": null, "premium": false}
Migrated: {"email": "alice@example.com", "age": 30, "account_status": "active", "created_at": "2024-01-01", "last_login": null, "premium": false, "name": "Alice", "id": 1}

How it works

The expand_contract function iterates over rename rules, mapping old keys to new names, and uses setdefault to add new fields with sensible defaults. The migrate_contract function uses deepcopy to avoid mutating the input, then renames keys to produce the final contract version. This two-step pattern keeps field additions separate from schema migrations, making each step testable. The reliance on setdefault guarantees backward compatibility for records that already contain some new fields.

Common mistakes

  • Forgetting `deepcopy` and mutating the original data during migration
  • Assuming every old key exists instead of using `.get()` or `if key in data`
  • Hard-coding defaults that don't match production values
  • Not versioning the contract, making rollback difficult

Variations

  1. Use a dataclass with `asdict()` for typed contract validation
  2. Apply a chain of migration functions with a version map for multi-step upgrades

Real-world use cases

  • Migrating legacy user records to a new schema when rolling out a platform-wide API version.
  • Expanding event payloads in a Kafka pipeline to add optional fields without breaking old consumers.
  • Upgrading database records during a live deployment by applying rename and default-value rules.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Production deployment patterns

Related tutorials and quizzes for this topic.