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.
Python code
50 linesimport 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
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
- Use a dataclass with `asdict()` for typed contract validation
- 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
More from Production deployment patterns
- Auto Rollback on Error Rate Exceeded in Python medium
- Automate Semantic Versioning with Conventional Commits in Python medium
- Design a Data Helper for Beginners in Python easy
- Docker healthcheck CMD mock in Python easy
- Generate a Mock Artifact Version Tag in Python easy
- Generate a docker-compose.yml with mock services in Python easy
Keep learning
Related tutorials and quizzes for this topic.