Avoid mutable error details in a reusable exception
Also published in our primary archive.
Copy mutable error details at exception construction time so a reusable exception reports the state that caused it. The input here is {'row': 2}. A legacy exception keeps that same dictionary reference, so changing the caller’s dictionary changes the old exception too. The corrected ValidationError stores dict(details), and therefore continues to report row 2 after the caller changes its own dictionary.
The ownership boundary is the constructor. Before the call, the caller owns and may legitimately update its mapping. After the call, the exception should own a snapshot of the diagnostic information it will expose. A shallow copy is enough for this fixture because the value is an immutable integer. It is not a deep immutability guarantee: nested lists or dictionaries remain shared. If nested data can be mutated, normalize it into immutable data, recursively copy it with carefully chosen semantics, or document that the exception stores a shallow snapshot.
The example uses a custom exception rather than relying on a string message alone, so callers can inspect error.details['row'] without parsing prose. The assertions contrast the two ownership choices and confirm the corrected object’s reported row remains 2. They do not establish thread safety or validate a serialization format.
This example uses no newer API; it runs on supported Python 3 versions.
AI assistance disclosure: this article was drafted with AI assistance and the synthetic example is intended to be run locally.
Source: Python’s errors and exceptions tutorial covers user-defined exceptions; dict() constructs a new dictionary from a mapping.
class LegacyValidationError(Exception):
def __init__(self, details):
self.details = details
super().__init__("validation failed")
class ValidationError(Exception):
def __init__(self, details):
self.details = dict(details)
super().__init__("validation failed")
caller_details = {"row": 2}
legacy_error = LegacyValidationError(caller_details)
error = ValidationError(caller_details)
caller_details["row"] = 9
assert legacy_error.details["row"] == 9
assert error.details == {"row": 2}
print(f"legacy row: {legacy_error.details['row']}")
print(f"copied row: {error.details['row']}")
legacy row: 9
copied row: 2