Validation and Data Integrity with Properties
15 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Defensive programming ensures your objects catch errors at the perimeter before corrupt data poisons databases or downstream microservices. Properties are Python’s ideal gatekeepers for validation and data integrity.
Type and Range Validation
Validation is checking a value against the rules before storing it. In a property setter, a bad value raises an error and the object keeps its last good value.
Check both the data type and valid range inside setters.
class Student: def __init__(self, score): self.score = score # routes through setter
@property def score(self): return self._score
@score.setter def score(self, value): if not isinstance(value, (int, float)): raise TypeError(f"Score must be numeric, got {type(value).__name__}") if not (0 <= value <= 100): raise ValueError(f"Score must be between 0 and 100, got {value}") self._score = valueExample 1 · Type and range checks on marks
class Marks: def __init__(self, score): self.score = score
@property def score(self): return self._score
@score.setter def score(self, value): if not isinstance(value, (int, float)): raise TypeError("score must be a number") if not 0 <= value <= 100: raise ValueError("score must be 0 to 100") self._score = value
for raw in (85, 101, "90"): try: print("accepted", Marks(raw).score) except (TypeError, ValueError) as e: print("rejected", repr(raw), "-", e)accepted 85 rejected 101 - score must be 0 to 100 rejected '90' - score must be a number
What this code does
- The wrong type raises TypeError; a right type with a bad value raises ValueError.
- The loop shows all three cases in one run.
Attempting to assign "ninety" or 105 raises an informative exception immediately.
Input Sanitization in Setters
Setters can clean and normalize incoming data before storing it.
class UserProfile: @property def username(self): return self._username
@username.setter def username(self, value): if not isinstance(value, str): raise TypeError("Username must be a string") clean = value.strip().lower() if len(clean) < 3: raise ValueError("Username must be at least 3 characters") self._username = cleanExample 2 · Cleaning an email before storing it
class Customer: def __init__(self, email): self.email = email
@property def email(self): return self._email
@email.setter def email(self, value): clean = value.strip().lower() if "@" not in clean: raise ValueError("invalid email") self._email = clean
c = Customer(" Asha@Example.COM ")print(c.email)c.email = "ravi@example.com"print(c.email)asha@example.com ravi@example.com
What this code does
- The setter trims spaces and lowercases the text, so every stored email has one form.
- Callers never need to remember to clean the text themselves.
Passing " AaravDev " is automatically stored as "aaravdev".
Multi-Attribute Invariants
Invariant is a condition that must stay true for the whole life of an object, such as start <= end or balance >= 0. Setters and methods work together to protect it.
An invariant is a condition that must remain true throughout the object’s lifetime (e.g. start_date <= end_date). Properties allow keeping dependent attributes in sync.
- Validate relationships when either dependent attribute changes.
- Keep internal state consistent and prevent impossible combinations.
- Keep public API intuitive for clients.
Always Route __init__ Through Setters
Inside __init__, assign to self.attribute (the property) rather than self._attribute directly.
def __init__(self, name, score): self.name = name self.score = score # Executes the @score.setter validation!Example 3 · A bad object is never created
class OrderLine: def __init__(self, sku, qty): self.sku = sku self.qty = qty
@property def qty(self): return self._qty
@qty.setter def qty(self, value): if value < 1: raise ValueError("qty must be at least 1") self._qty = value
try: OrderLine("A-100", 0)except ValueError as e: print("order failed:", e)
line = OrderLine("A-100", 2)print(line.qty)order failed: qty must be at least 1 2
What this code does
__init__assigns throughself.qty, so the same rule covers creation and later changes.- If the constructor raises, no half-valid object exists.
This guarantees that new objects cannot be instantiated with invalid values.
Next step
Check your understanding
12 quick questions